Objects
Validates Map values against a fixed set of named fields. Extra keys are silently stripped from the output by default. Validation is exhaustive, all field failures are collected before returning.
Signatures:
ZemaObject<Map<String, dynamic>> object(Map<String, ZemaSchema> shape)
ZemaObject<T> objectAs(Map<String, ZemaSchema> shape, T Function(Map<String, dynamic>) constructor)
Validation order: type check → field validation (exhaustive) → strict key check (if enabled) → constructor (if provided).
Basic usage
final schema = z.object({
'name': z.string(),
'age': z.integer(),
});
schema.parse({'name': 'Alice', 'age': 30});
// {'name': 'Alice', 'age': 30}
schema.parse({'name': 'Alice', 'age': 'thirty'});
// throws ZemaException: invalid_type at path 'age'
Missing and optional fields
A key absent from the input arrives as null. Whether that passes depends on the field's schema:
z.string() // fails — null is not a String
z.string().optional() // passes — output is null
z.string().withDefault('anon') // passes — output is 'anon'
final schema = z.object({
'name': z.string(),
'phone': z.string().optional(),
'role': z.string().withDefault('user'),
});
schema.parse({'name': 'Alice'});
// {'name': 'Alice', 'phone': null, 'role': 'user'}
Nullable fields
final schema = z.object({
'name': z.string(),
'bio': z.string().nullable(),
});
schema.parse({'name': 'Alice', 'bio': null});
// {'name': 'Alice', 'bio': null}
Typed output with objectAs
Maps the validated Map<String, dynamic> to a custom class:
final userSchema = z.objectAs(
{
'name': z.string().min(2),
'email': z.string().email(),
},
(map) => User(name: map['name'], email: map['email']),
);
final user = userSchema.parse(json); // User, not a Map
If the constructor throws, a transform_error issue is produced.
Nested objects
final schema = z.object({
'user': z.object({
'name': z.string(),
'email': z.string().email(),
}),
'metadata': z.object({
'createdAt': z.dateTime(),
'version': z.integer(),
}),
});
Schema composition
.extend(shape)
Adds or overrides fields. Returns Map<String, dynamic>.
final baseSchema = z.object({
'id': z.integer(),
'createdAt': z.dateTime(),
});
final userSchema = baseSchema.extend({
'name': z.string(),
'email': z.string().email(),
});
// userSchema validates: id, createdAt, name, email
.pick(keys)
Returns a new schema with only the listed fields.
final fullSchema = z.object({
'id': z.integer(),
'name': z.string(),
'email': z.string().email(),
'password': z.string(),
});
final publicSchema = fullSchema.pick(['id', 'name', 'email']);
// password excluded
.omit(keys)
Returns a new schema with the listed fields removed.
final publicSchema = fullSchema.omit(['password']);
// id, name, email kept
Strict mode
By default, unknown keys are stripped from the output. Call .makeStrict() to reject them with an unknown_key issue.
final schema = z.object({
'name': z.string(),
}).makeStrict();
schema.parse({'name': 'Alice'}); // OK
schema.parse({'name': 'Alice', 'extra': 'value'}); // throws ZemaException: unknown_key
Refinements
// Cross-field validation — passwords must match
final schema = z.object({
'password': z.string().min(8),
'confirmPassword': z.string(),
}).refine(
(data) => data['password'] == data['confirmPassword'],
message: 'Passwords must match',
);
schema.parse({
'password': 'secret123',
'confirmPassword': 'different',
}); // throws ZemaException
Error paths
Field-level issues include the field name in their path:
final result = z.object({
'name': z.string(),
'email': z.string().email(),
}).safeParse({'name': 'Alice', 'email': 'bad'});
switch (result) {
case ZemaFailure(:final errors):
for (final issue in errors) {
print('${issue.pathString}: ${issue.message}');
// 'email': Must be a valid email address
}
}
Nested paths format as 'address.zip' or 'items.[0].name'.
Common patterns
Registration form
final registrationSchema = z.object({
'username': z.string()
.trim()
.min(3)
.max(20)
.regex(RegExp(r'^[a-zA-Z0-9_]+$'), message: 'Alphanumeric and underscores only.'),
'email': z.string().email(),
'password': z.string()
.min(8)
.regex(RegExp(r'(?=.*[0-9])'), message: 'Must contain at least one digit.'),
'confirmPassword': z.string(),
}).refine(
(data) => data['password'] == data['confirmPassword'],
message: 'Passwords must match',
);
API response
final userSchema = z.object({
'id': z.integer(),
'username': z.string(),
'email': z.string().email(),
'profile': z.object({
'avatar': z.string().url().nullable(),
'bio': z.string().max(500).optional(),
}),
'createdAt': z.dateTime(),
});
Configuration with defaults
final configSchema = z.object({
'database': z.object({
'host': z.string(),
'port': z.integer().gte(1).lte(65535),
'ssl': z.boolean().withDefault(true),
}),
'server': z.object({
'port': z.integer().withDefault(8080),
'host': z.string().withDefault('localhost'),
'corsOrigins': z.array(z.string()).withDefault([]),
}),
});
Error codes
| Code | Trigger |
|---|---|
invalid_type | Input is not a Map. |
unknown_key | Extra key present in strict mode. |
transform_error | objectAs constructor threw. |
API reference
| Method | Description |
|---|---|
.extend(shape) | Add or override fields. |
.pick(keys) | Keep only the listed fields. |
.omit(keys) | Remove the listed fields. |
.makeStrict() | Reject unknown keys (unknown_key). |
.map(fn) | Transform the validated map to a new type. |
.optional() | Allow null input. |
.withDefault(v) | Substitute null with v. |
.refine(fn) | Add a custom cross-field validation rule. |