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.

Must be a positive even number

Integer between 1 and 100

Must begin with "BATCH-"


Code Svelte
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.

Rules are checked top-to-bottom, one error shown at a time

4-6 digits, not all identical, not a common sequence


Code Svelte
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.

3 or 6 digit hex color code

Valid IPv4 address

Semantic versioning format

With or without the @ prefix


Code Svelte
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.

Luhn algorithm validated (try: 4111 1111 1111 1111)

13-digit barcode with checksum validation

US or Canadian format


Code Svelte
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.


Code Svelte
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" />