Schema.filter — Custom Predicates
Schema.filter(predicate, { message: () => '...' }) adds arbitrary
validation logic beyond built-in string/number constraints. Return true for
valid or let it evaluate to false to trigger the message.
1
2<script lang="ts">
3 import { Schema, pipe } from "effect";
4 import { SchemaForm, FormController, withField, withFormLayout } from "@kareyes/aether/forms";
5
6 // Schema.filter(predicate, { message: () => "..." }) adds custom validation logic
7 // beyond built-in string/number constraints.
8 // Return true (or let it evaluate truthy) = valid.
9 // Return false (or falsy) = invalid — triggers the message.
10 const FilterSchema = pipe(
11 Schema.Struct({
12 quantity: pipe(
13 Schema.Number,
14 Schema.filter((n) => n > 0 && n % 2 === 0, {
15 message: () => "Quantity must be a positive even number"
16 }),
17 withField({ label: "Quantity (even only)", inputType: "number", colSpan: 6 })
18 ),
19 units: pipe(
20 Schema.Number,
21 Schema.filter((n) => Number.isInteger(n) && n >= 1 && n <= 100, {
22 message: () => "Units must be a whole number between 1 and 100"
23 }),
24 withField({ label: "Units (1–100)", inputType: "number", colSpan: 6 })
25 ),
26 batchCode: pipe(
27 Schema.String,
28 Schema.filter((s) => s.toUpperCase().startsWith("BATCH-"), {
29 message: () => 'Batch code must start with "BATCH-"'
30 }),
31 withField({ label: "Batch Code", placeholder: "BATCH-001" })
32 )
33 }),
34 withFormLayout({
35 columns: 12,
36 sections: [{ id: "main", title: "Custom Filter Predicates" }]
37 })
38 );
39
40 const controller = new FormController(FilterSchema, { validateOnBlur: true });
41</script>
42
43<SchemaForm {controller} onSubmit={(d) => console.log(d)} submitText="Submit Order" />Multi-Rule Piped Filters
Chain multiple Schema.filter calls in a pipe — each rule gets its own error
message. Validation runs top-to-bottom and stops at the first failure, giving the user
targeted, actionable feedback.
1
2<script lang="ts">
3 import { Schema, pipe } from "effect";
4 import { SchemaForm, FormController, withField, withFormLayout } from "@kareyes/aether/forms";
5
6 // Chain multiple Schema.filter calls in a pipe.
7 // Each rule has its own specific error message.
8 // Validation runs top-to-bottom and stops at the first failure —
9 // giving the user one targeted, actionable error at a time.
10 const PasswordRulesSchema = pipe(
11 Schema.Struct({
12 password: pipe(
13 Schema.String,
14 Schema.filter((s) => s.length >= 8, {
15 message: () => "At least 8 characters required"
16 }),
17 Schema.filter((s) => /[A-Z]/.test(s), {
18 message: () => "Must contain at least one uppercase letter"
19 }),
20 Schema.filter((s) => /[a-z]/.test(s), {
21 message: () => "Must contain at least one lowercase letter"
22 }),
23 Schema.filter((s) => /\d/.test(s), {
24 message: () => "Must contain at least one number"
25 }),
26 Schema.filter((s) => /[!@#$%^&*]/.test(s), {
27 message: () => "Must contain at least one special character (!@#$%^&*)"
28 }),
29 withField({
30 label: "Password",
31 inputType: "password",
32 description: "Rules checked top-to-bottom — one error shown at a time"
33 })
34 ),
35 pin: pipe(
36 Schema.String,
37 Schema.filter((s) => /^\d{4,6}$/.test(s), {
38 message: () => "PIN must be 4-6 digits"
39 }),
40 Schema.filter((s) => !/^(\d)\1+$/.test(s), {
41 message: () => "PIN cannot be all the same digit"
42 }),
43 Schema.filter((s) => !["1234", "0000", "1111", "123456"].includes(s), {
44 message: () => "PIN is too common, please choose a different one"
45 }),
46 withField({
47 label: "PIN",
48 inputType: "password",
49 description: "4-6 digits, not all identical, not a common sequence"
50 })
51 )
52 }),
53 withFormLayout({
54 columns: 1,
55 sections: [{ id: "main", title: "Multi-Rule Piped Filters" }]
56 })
57 );
58
59 const controller = new FormController(PasswordRulesSchema, { validateOnBlur: true });
60</script>
61
62<SchemaForm {controller} onSubmit={(d) => console.log(d)} submitText="Set Credentials" />Format & Structure Validation
Use Schema.filter to validate complex structural formats like hex colors, IP
addresses, semantic versions, and social handles. Internal normalization (trimming spaces,
handling @ prefixes) improves UX without sacrificing correctness.
1
2<script lang="ts">
3 import { Schema, pipe } from "effect";
4 import { SchemaForm, FormController, withField, withFormLayout } from "@kareyes/aether/forms";
5
6 // Schema.filter is ideal for structured format validation.
7 // Internal normalization (stripping spaces, handling optional prefixes)
8 // allows flexible user input while still enforcing correctness.
9 const FormatSchema = pipe(
10 Schema.Struct({
11 hex: pipe(
12 Schema.String,
13 Schema.filter((s) => /^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/.test(s), {
14 message: () => "Enter a valid hex color (e.g., #FF5733 or #F53)"
15 }),
16 withField({ label: "Hex Color", placeholder: "#FF5733" })
17 ),
18 ipAddress: pipe(
19 Schema.String,
20 Schema.filter(
21 (s) =>
22 /^(\d{1,3}\.){3}\d{1,3}$/.test(s) &&
23 s.split(".").every((n) => parseInt(n) <= 255),
24 { message: () => "Enter a valid IPv4 address (e.g., 192.168.1.1)" }
25 ),
26 withField({ label: "IP Address", placeholder: "192.168.1.1" })
27 ),
28 semver: pipe(
29 Schema.String,
30 Schema.filter(
31 (s) => /^\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?(\+[a-zA-Z0-9.]+)?$/.test(s),
32 { message: () => "Enter a valid semver (e.g., 1.2.3 or 2.0.0-beta.1)" }
33 ),
34 withField({ label: "Version (semver)", placeholder: "1.0.0" })
35 ),
36 twitterHandle: pipe(
37 Schema.String,
38 Schema.filter(
39 (s) => {
40 const handle = s.startsWith("@") ? s.slice(1) : s;
41 return (
42 handle.length > 0 &&
43 handle.length <= 15 &&
44 /^[a-zA-Z0-9_]+$/.test(handle)
45 );
46 },
47 { message: () => "Handle: max 15 chars, letters/numbers/underscores (@ optional)" }
48 ),
49 withField({ label: "Twitter Handle", placeholder: "@johndoe" })
50 )
51 }),
52 withFormLayout({
53 columns: 1,
54 sections: [{ id: "main", title: "Format Validation" }]
55 })
56 );
57
58 const controller = new FormController(FormatSchema, { validateOnBlur: true });
59</script>
60
61<SchemaForm {controller} onSubmit={(d) => console.log(d)} submitText="Validate" />Domain-Specific Refinements
Embed real business logic — like the Luhn algorithm for credit cards, EAN-13 checksum for barcodes, or multi-format postal codes — directly in the schema. The schema becomes the single source of truth for all data correctness rules.
1
2<script lang="ts">
3 import { Schema, pipe } from "effect";
4 import { SchemaForm, FormController, withField, withFormLayout } from "@kareyes/aether/forms";
5
6 // Embed real business logic directly in the schema.
7 // The schema becomes the single source of truth for data correctness.
8 const DomainSchema = pipe(
9 Schema.Struct({
10 creditCard: pipe(
11 Schema.String,
12 Schema.filter(
13 (s) => {
14 const digits = s.replace(/[\s-]/g, "");
15 if (!/^\d{13,19}$/.test(digits)) return false;
16 // Luhn algorithm check
17 let sum = 0;
18 let alt = false;
19 for (let i = digits.length - 1; i >= 0; i--) {
20 let n = parseInt(digits[i]);
21 if (alt) { n *= 2; if (n > 9) n -= 9; }
22 sum += n;
23 alt = !alt;
24 }
25 return sum % 10 === 0;
26 },
27 { message: () => "Invalid card number — please double-check your card" }
28 ),
29 withField({
30 label: "Credit Card Number",
31 placeholder: "4111 1111 1111 1111",
32 description: "Luhn algorithm validated (try: 4111 1111 1111 1111)"
33 })
34 ),
35 postalCode: pipe(
36 Schema.String,
37 Schema.filter(
38 (s) =>
39 /^\d{5}(-\d{4})?$/.test(s) ||
40 /^[A-Z]\d[A-Z] \d[A-Z]\d$/.test(s.toUpperCase()),
41 { message: () => "Enter a US (12345) or Canadian (A1A 1A1) postal code" }
42 ),
43 withField({ label: "Postal Code", placeholder: "12345 or A1A 1A1" })
44 )
45 }),
46 withFormLayout({
47 columns: 1,
48 sections: [{ id: "main", title: "Domain-Specific Refinements" }]
49 })
50 );
51
52 const controller = new FormController(DomainSchema, { validateOnBlur: true });
53</script>
54
55<SchemaForm {controller} onSubmit={(d) => console.log(d)} submitText="Validate Fields" />Required vs Optional Fields
Fields with no validation constraints (plain Schema.String or Schema.Boolean) are effectively optional — they accept any value and never
block form submission. Only add constraints when the data truly requires them.
1
2<script lang="ts">
3 import { Schema, pipe } from "effect";
4 import { SchemaForm, FormController, withField, withFormLayout } from "@kareyes/aether/forms";
5
6 // Fields with no validation constraints are effectively optional —
7 // they accept any value and never block form submission.
8 // Only add Schema constraints when the data truly requires them.
9 const ProfileSchema = pipe(
10 Schema.Struct({
11 // Required: minLength(1) ensures the field is non-empty
12 requiredName: pipe(
13 Schema.String,
14 Schema.minLength(1),
15 Schema.annotations({ message: () => "Name is required" }),
16 withField({ label: "Full Name (required)", colSpan: 6 })
17 ),
18 // Optional: plain Schema.String — any value (including empty) is valid
19 nickname: pipe(
20 Schema.String,
21 withField({ label: "Nickname (optional)", placeholder: "How friends call you", colSpan: 6 })
22 ),
23 // Optional URL — no constraints enforced
24 website: pipe(
25 Schema.String,
26 withField({ label: "Website (optional)", inputType: "url" })
27 ),
28 // Schema.Boolean defaults to false — always optional
29 newsletter: pipe(
30 Schema.Boolean,
31 withField({ label: "Subscribe to newsletter", inputType: "switch" })
32 )
33 }),
34 withFormLayout({
35 columns: 12,
36 sections: [{ id: "main", title: "Required vs Optional Fields" }]
37 })
38 );
39
40 const controller = new FormController(ProfileSchema, {
41 initialValues: { newsletter: false }
42 });
43</script>
44
45<SchemaForm {controller} onSubmit={(d) => console.log(d)} submitText="Save Profile" />