Preprocess
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
preprocess | transform | |
|---|---|---|
| Runs | Before validation | After validation |
| Input | Raw, loosely typed | Validated, typed Output |
| Purpose | Normalize / coerce input | Map valid output to another type |
| On failure | preprocess_error | transform_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):
preprocessor(rawInput)runs — produces intermediate value.- If
preprocessorthrows →preprocess_errorissue returned immediately. - Base schema validates the intermediate value.
- 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
| Code | Trigger |
|---|---|
preprocess_error | The 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 I | The raw input type the returned schema accepts. Use dynamic for untyped inputs. |
fn | Called with the raw input. Its return value is passed to the base schema. If it throws, preprocess_error is produced. |
| Returns | A new schema with input type I and the same output type as the original. |