Zema Logo
Schemas

Modifiers

Adjust how any schema handles null, absence, and failures with .optional(), .nullable(), .withDefault(), and .catchError().

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:

InputOutput
nullsuccess(null) — base schema skipped
non-nullresult 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:

InputOutput
nullsuccess(null) — base schema skipped
non-nullresult 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()
IntentField may be absent (key not present in JSON)Field is present but its value is explicitly null
RuntimeIdenticalIdentical
Output typeO?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:

InputBase resultOutput
null— (skipped)success(defaultValue)
non-nullsuccesssuccess(base output)
non-nullfailuresuccess(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 resultOutput
successforwarded unchanged
failuresuccess(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 typeStatic valueDynamic: computed from issues
Access to issuesNoYes: full List<ZemaIssue>
Accepts null inputYes (I?)No (I)
Best forConfig defaults, missing fieldsLogging, 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
ReturnsZemaSchema<I?, O?>
Null inputsuccess(null)
Non-null inputDelegates to base schema

.nullable()

Description
ReturnsZemaSchema<I?, O?>
Null inputsuccess(null)
Non-null inputDelegates to base schema

.withDefault(value)

ParameterTypeDescription
valueOReturned when input is null or base schema fails
ReturnsZemaSchema<I?, O>Output is never null

.catchError(handler)

ParameterTypeDescription
handlerO Function(List<ZemaIssue>)Receives the failure's issue list; returns the fallback value
ReturnsZemaSchema<I, O>Input type unchanged (not widened to I?)

Next steps

Copyright © 2026