Zema Logo
Transformations

Preprocess

Transform raw input before validation runs with .preprocess(): normalize, coerce, and sanitize data upstream of your schema.

preprocess() transforms the raw input before the schema validates it. Use it to normalize messy data: trimming strings, coercing types, renaming fields, so validation runs on clean input.

Signature:

ZemaSchema<I, Output> preprocess<I>(Input Function(I) fn)

preprocess() is called at the end of the schema chain. The function receives the raw input and returns the value that the base schema will validate.


Preprocess vs transform

preprocesstransform
RunsBefore validationAfter validation
InputRaw, loosely typedValidated, typed Output
PurposeNormalize / coerce inputMap valid output to another type
On failurepreprocess_errortransform_error

Basic usage

// Trim whitespace before the min-length check
final schema = z.string()
    .min(3)
    .preprocess<dynamic>((v) => v?.toString().trim() ?? '');

schema.parse('  hi  ');   // throws ZemaException: 'hi' has length 2 < 3
schema.parse('  hey ');   // 'hey'

Note the order: constraints are defined first, .preprocess() is the last call. The function runs first at parse time.


Execution order

For schema.parse(rawInput):

  1. preprocessor(rawInput) runs — produces intermediate value.
  2. If preprocessor throws → preprocess_error issue returned immediately.
  3. Base schema validates the intermediate value.
  4. Base schema result is returned.

Type parameter

preprocess<I> declares the raw input type I. Use dynamic when the raw input can be anything:

z.string().email().preprocess<dynamic>((v) => v?.toString().trim() ?? '')
//                              ↑ raw input type

Common patterns

Trim whitespace

final emailSchema = z.string()
    .email()
    .preprocess<dynamic>((v) => v is String ? v.trim() : v);

emailSchema.parse('  alice@example.com  ');   // 'alice@example.com'

Coerce a field that may arrive as a string

final ageSchema = z.integer()
    .gte(0)
    .lte(120)
    .preprocess<dynamic>((v) => v is String ? int.tryParse(v) ?? v : v);

ageSchema.parse('28');   // 28
ageSchema.parse(28);     // 28
ageSchema.parse('abc');  // throws ZemaException: invalid_type (int.tryParse returned null, original 'abc' passed through)

Normalize a phone number

final phoneSchema = z.string()
    .regex(RegExp(r'^\d{10}$'), message: 'Must be 10 digits.')
    .preprocess<dynamic>((v) => v is String
        ? v.replaceAll(RegExp(r'\D'), '')
        : v);

phoneSchema.parse('(555) 123-4567');   // '5551234567'
phoneSchema.parse('5551234567');       // '5551234567'

Case-insensitive enum

final statusSchema = z.string()
    .oneOf(['pending', 'active', 'archived'])
    .preprocess<dynamic>((v) => v is String ? v.toLowerCase() : v);

statusSchema.parse('ACTIVE');    // 'active'
statusSchema.parse('Pending');   // 'pending'

Default for null input

final countSchema = z.integer()
    .gte(0)
    .preprocess<dynamic>((v) => v ?? 0);

countSchema.parse(null);   // 0
countSchema.parse(5);      // 5

For null substitution, prefer .withDefault(v) when the fallback is a constant, it is more explicit.


Object preprocessing

Add a computed field

final userSchema = z.object({
  'id': z.string(),
  'createdAt': z.dateTime(),
}).preprocess<dynamic>((value) {
  if (value is! Map) return value;
  final map = Map<String, dynamic>.from(value);
  map['createdAt'] ??= DateTime.now().toIso8601String();
  return map;
});

userSchema.parse({'id': '123'});
// {'id': '123', 'createdAt': DateTime(...)}

Rename a field (API compatibility)

final schema = z.object({
  'email': z.string().email(),
  'fullName': z.string(),
}).preprocess<dynamic>((value) {
  if (value is! Map) return value;
  final map = Map<String, dynamic>.from(value);
  if (map.containsKey('name') && !map.containsKey('fullName')) {
    map['fullName'] = map.remove('name');
  }
  return map;
});

schema.parse({
  'email': 'alice@example.com',
  'name': 'Alice Smith',   // API v1 field name
});
// {'email': 'alice@example.com', 'fullName': 'Alice Smith'}

Flatten a nested structure

final schema = z.object({
  'userId': z.string(),
  'userName': z.string(),
}).preprocess<dynamic>((value) {
  if (value is! Map || !value.containsKey('user')) return value;
  final user = value['user'] as Map;
  return {'userId': user['id'], 'userName': user['name']};
});

schema.parse({'user': {'id': '123', 'name': 'Alice'}});
// {'userId': '123', 'userName': 'Alice'}

Array preprocessing

Wrap a single value in a list

final tagsSchema = z.array(z.string())
    .preprocess<dynamic>((value) {
  if (value is String) return [value];
  if (value is List) return value;
  return [];
});

tagsSchema.parse('dart');          // ['dart']
tagsSchema.parse(['dart', 'flutter']);  // ['dart', 'flutter']
tagsSchema.parse(null);            // []

Remove duplicates before a length check

final schema = z.array(z.string())
    .max(5)
    .preprocess<dynamic>((v) => v is List ? v.toSet().toList() : v);

Combining preprocess and transform

final schema = z.string()
    .min(3)
    .transform((s) => s.toUpperCase())
    .preprocess<dynamic>((v) => v?.toString().trim() ?? '');

schema.parse('  hello  ');
// Step 1 (preprocess): '  hello  ' → 'hello'
// Step 2 (validate):    'hello'.length >= 3 ✓
// Step 3 (transform):   'hello' → 'HELLO'
// Result: 'HELLO'

Error handling

If the preprocessor throws, a preprocess_error issue is returned immediately, the base schema is never called:

final schema = z.integer().preprocess<dynamic>((v) {
  if (v == null) throw ArgumentError('null not allowed');
  return v;
});

final result = schema.safeParse(null);
// ZemaFailure: ZemaIssue(code: 'preprocess_error', message: 'Preprocessing failed: ...')

Error codes

CodeTrigger
preprocess_errorThe preprocessor function threw.

Plus any codes produced by the base schema after preprocessing.


API reference

ZemaSchema<I, Output> preprocess<I>(Input Function(I) fn)
Description
Type param IThe raw input type the returned schema accepts. Use dynamic for untyped inputs.
fnCalled with the raw input. Its return value is passed to the base schema. If it throws, preprocess_error is produced.
ReturnsA new schema with input type I and the same output type as the original.
Copyright © 2026