Modifiers
Modifiers wrap an existing schema to change how it handles null, absence, and failures. All modifier methods are available on every ZemaSchema.
.optional()
Treats null as a valid input and passes it through as null. Non-null values are delegated to the base schema.
Signature:
ZemaSchema<I?, O?> optional()
Behaviour:
| Input | Output |
|---|---|
null | success(null) — base schema skipped |
| non-null | result of base.safeParse(value) |
The output type widens from O to O?.
Basic usage
final schema = z.string().optional();
schema.parse(null); // null
schema.parse('hello'); // 'hello'
schema.parse(42); // throws ZemaException: invalid_type
In an object
Missing keys in a ZemaObject arrive as null. Mark a field .optional() to allow omission:
final schema = z.object({
'name': z.string(),
'nickname': z.string().min(2).optional(),
});
schema.parse({'name': 'Alice'}); // nickname → null, OK
schema.parse({'name': 'Alice', 'nickname': 'X'}); // throws: too_short
schema.parse({'name': 'Alice', 'nickname': 'Al'}); // OK
.nullable()
Treats null as a distinct, explicitly-present value. Behaviour is identical to .optional() at runtime.
Signature:
ZemaSchema<I?, O?> nullable()
Behaviour:
| Input | Output |
|---|---|
null | success(null) — base schema skipped |
| non-null | result of base.safeParse(value) |
Basic usage
final schema = z.string().nullable();
schema.parse(null); // null
schema.parse('hello'); // 'hello'
schema.parse(42); // throws ZemaException: invalid_type
optional vs nullable
Both accept null. The distinction is intent, not runtime behavior:
.optional() | .nullable() | |
|---|---|---|
| Intent | Field may be absent (key not present in JSON) | Field is present but its value is explicitly null |
| Runtime | Identical | Identical |
| Output type | O? | O? |
// optional — field can be omitted entirely
z.string().optional()
// nullable — field is present; null is a valid value
z.string().nullable()
Use .optional() for JSON keys that may not exist. Use .nullable() for keys that are always sent but may carry a null value.
.withDefault(value)
Substitutes a static fallback value whenever the input is null or the base schema fails. The output type is O (non-nullable), the fallback guarantees a value in every case.
Signature:
ZemaSchema<I?, O> withDefault(O value)
Behaviour:
| Input | Base result | Output |
|---|---|---|
null | — (skipped) | success(defaultValue) |
| non-null | success | success(base output) |
| non-null | failure | success(defaultValue): silently swallowed |
Basic usage
final schema = z.string().withDefault('anonymous');
schema.parse(null); // 'anonymous'
schema.parse('Alice'); // 'Alice'
Config fields
final configSchema = z.object({
'timeout': z.integer().withDefault(30),
'retries': z.integer().withDefault(3),
'debug': z.boolean().withDefault(false),
});
configSchema.parse({});
// {timeout: 30, retries: 3, debug: false}
Silent failure behaviour
.withDefault() also returns the default when the base schema fails on a non-null input. Validation errors are discarded without surfacing. If you need to inspect the issues before falling back, use .catchError() instead:
// withDefault — errors discarded silently
z.integer().withDefault(-1).parse('oops'); // -1, no diagnostics
// catchError — errors visible before fallback
z.integer().catchError((issues) {
logger.warn(issues.first.message);
return -1;
}).parse('oops'); // -1, with logging
.catchError(handler)
Intercepts failures from the base schema and converts them into a fallback success value via a handler function. The schema always succeeds, failures are never propagated.
Signature:
ZemaSchema<I, O> catchError(O Function(List<ZemaIssue>) handler)
Behaviour:
| Base result | Output |
|---|---|
| success | forwarded unchanged |
| failure | success(handler(issues)): failure swallowed |
Unlike .withDefault(), the input type stays I (not I?): .catchError() does not accept null inputs. Combine with .optional() or .nullable() if null handling is also needed.
Basic usage
final schema = z.integer().catchError((_) => -1);
schema.parse(42); // 42
schema.parse('oops'); // -1
Inspecting issues before fallback
The handler receives the full list of ZemaIssues:
final schema = z.string().email().catchError((issues) {
final isTypeError = issues.any((e) => e.code == 'invalid_type');
return isTypeError ? '' : 'fallback@example.com';
});
schema.parse(42); // '' (type error → empty string)
schema.parse('not-an-email'); // 'fallback@example.com'
Logging fallbacks
final schema = z.integer().gte(0).catchError((issues) {
logger.warn('Falling back: ${issues.first.message}');
return 0;
});
withDefault vs catchError
.withDefault(v) | .catchError(fn) | |
|---|---|---|
| Fallback type | Static value | Dynamic: computed from issues |
| Access to issues | No | Yes: full List<ZemaIssue> |
Accepts null input | Yes (I?) | No (I) |
| Best for | Config defaults, missing fields | Logging, conditional fallbacks |
Combining modifiers
Modifiers compose: chain them in any order that reads naturally:
// Optional with a default: null → 'guest', non-null must be a valid email
z.string().email().withDefault('guest@example.com')
// Nullable with a fallback and logging
z.integer().nullable().catchError((issues) {
logger.warn(issues);
return null;
})
API reference
.optional()
| Description | |
|---|---|
| Returns | ZemaSchema<I?, O?> |
| Null input | success(null) |
| Non-null input | Delegates to base schema |
.nullable()
| Description | |
|---|---|
| Returns | ZemaSchema<I?, O?> |
| Null input | success(null) |
| Non-null input | Delegates to base schema |
.withDefault(value)
| Parameter | Type | Description |
|---|---|---|
value | O | Returned when input is null or base schema fails |
| Returns | ZemaSchema<I?, O> | Output is never null |
.catchError(handler)
| Parameter | Type | Description |
|---|---|---|
handler | O Function(List<ZemaIssue>) | Receives the failure's issue list; returns the fallback value |
| Returns | ZemaSchema<I, O> | Input type unchanged (not widened to I?) |
Next steps
- Refinements: add custom validation rules to any schema
- Transforms: map the validated output to a new type
- Branded Types: nominal typing with
.brand<B>()