Transforms
transform
Maps the validated output to a new type. The transformer function runs after validation succeeds. If the base schema fails, the transformer is never called.
Signature:
ZemaSchema<Input, T> transform<T>(T Function(Output) fn)
Execution order:
- Base schema validates input →
Output. - If validation fails → errors returned, transformer skipped.
- If validation succeeds →
fn(output)called. - If
fnthrows →transform_errorissue returned.
Basic usage
final schema = z.string().transform((s) => s.toUpperCase());
schema.parse('hello'); // 'HELLO'
schema.parse(42); // throws ZemaException: invalid_type
String to number
final schema = z.string()
.regex(RegExp(r'^\d+$'), message: 'Must be numeric.')
.transform(int.parse);
schema.parse('42'); // 42
schema.parse('abc'); // throws ZemaException: invalid_format (before transform)
String to DateTime
final schema = z.string()
.regex(RegExp(r'^\d{4}-\d{2}-\d{2}'))
.transform(DateTime.parse);
schema.parse('2024-02-08'); // DateTime(2024, 2, 8)
Or use z.dateTime() directly — it already handles ISO 8601 strings, Unix timestamps, and DateTime objects:
z.dateTime().parse('2024-02-08T10:30:00Z'); // DateTime
z.dateTime().parse(1707389400000); // DateTime
Timestamp to DateTime
// Milliseconds since epoch
final msSchema = z.integer()
.transform((ms) => DateTime.fromMillisecondsSinceEpoch(ms));
msSchema.parse(1707389400000); // DateTime
// Seconds since epoch
final sSchema = z.integer()
.transform((s) => DateTime.fromMillisecondsSinceEpoch(s * 1000));
String to Dart enum
enum UserRole { admin, user, guest }
final roleSchema = z.string()
.oneOf(['admin', 'user', 'guest'])
.transform((s) => UserRole.values.byName(s));
roleSchema.parse('admin'); // UserRole.admin
Map to typed model
final userSchema = z.object({
'id': z.integer(),
'email': z.string().email(),
}).transform((map) => User(
id: map['id'] as int,
email: map['email'] as String,
));
userSchema.parse({'id': 1, 'email': 'alice@example.com'});
// User(id: 1, email: 'alice@example.com')
For this pattern, z.objectAs() is a shorter alternative:
final userSchema = z.objectAs(
{'id': z.integer(), 'email': z.string().email()},
(map) => User(id: map['id'], email: map['email']),
);
Chaining transforms
Each .transform() call changes the output type. Transforms are applied in order:
final schema = z.string()
.transform((s) => s.trim())
.transform((s) => s.toLowerCase())
.transform((s) => s.replaceAll(' ', '-'));
schema.parse(' Hello World '); // 'hello-world'
Array transformations
// Map elements
z.array(z.string())
.transform((list) => list.map((s) => s.toUpperCase()).toList());
// Filter elements
z.array(z.integer())
.transform((list) => list.where((n) => n > 0).toList());
// Reduce to a scalar
z.array(z.integer())
.transform((list) => list.fold<int>(0, (sum, n) => sum + n));
// List → Map
z.array(z.object({'id': z.string(), 'name': z.string()}))
.transform((list) => {for (final item in list) item['id']: item['name']});
CSV string
final csvSchema = z.string()
.transform((s) => s.split(',').map((e) => e.trim()).toList());
csvSchema.parse('apple, banana, orange');
// ['apple', 'banana', 'orange']
Slug generation
final slugSchema = z.string()
.transform((title) => title
.toLowerCase()
.trim()
.replaceAll(RegExp(r'[^\w\s-]'), '')
.replaceAll(RegExp(r'\s+'), '-')
.replaceAll(RegExp(r'-+'), '-'));
slugSchema.parse('Hello World! 123'); // 'hello-world-123'
Transform error handling
If the transformer throws, transform_error is produced. Validate the input format first to ensure the transform is safe:
// ✓ Correct — validate format before transforming
final schema = z.string()
.regex(RegExp(r'^\d+$'), message: 'Must be numeric.')
.transform(int.parse); // safe: regex ensures parse succeeds
// ✗ Risky — no validation, 'abc' will cause transform_error
final schema = z.string().transform(int.parse);
pipe
Chains two schemas in sequence. The output of the first schema becomes the input of the second.
Signature:
ZemaSchema<Input, T> pipe<T>(ZemaSchema<Output, T> next)
Execution order:
- First schema validates input → intermediate value.
- If first fails → errors returned, second skipped.
- Second schema validates the intermediate value → final output.
transform vs pipe
Use .transform() to map a value to a new type with no further validation.
Use .pipe() when the intermediate value needs to pass through a full schema with its own rules.
// transform — simple mapping, no second schema
z.string().transform(int.parse)
// pipe — intermediate value goes through a real schema with constraints
z.string().transform(int.parse).pipe(z.integer().gte(0).lte(100))
Parse a numeric string and range-check
final portSchema = z.string()
.regex(RegExp(r'^\d+$'))
.transform(int.parse)
.pipe(z.integer().gte(1).lte(65535));
portSchema.parse('8080'); // 8080
portSchema.parse('99999'); // throws ZemaException: too_big
portSchema.parse('abc'); // throws ZemaException: invalid_format
Validate string then coerce
// Validate the string first, then run it through the coerce schema
z.string().regex(RegExp(r'^\d+$'))
.pipe(z.coerce().integer());
Date string → validated DateTime
final schema = z.string()
.pipe(z.dateTime()); // z.dateTime() accepts ISO 8601 strings
schema.parse('2024-02-08T10:30:00Z'); // DateTime
schema.parse('not-a-date'); // throws ZemaException: invalid_date
Error codes
| Code | Trigger |
|---|---|
transform_error | The transformer function threw an exception. |
Plus any codes produced by the base schema (or the second schema in pipe).
API reference
.transform(fn)
ZemaSchema<Input, T> transform<T>(T Function(Output) fn)
| Description | |
|---|---|
fn | Called with the validated output. If it throws, transform_error is returned. |
| Returns | New schema with output type T. |
.pipe(next)
ZemaSchema<Input, T> pipe<T>(ZemaSchema<Output, T> next)
| Description | |
|---|---|
next | A full schema that validates the current schema's output. |
| Returns | New schema with input type Input and output type T. |