Zema Logo
Schemas

Objects

Validate Map values with ZemaObject: fixed-shape validation, nested structures, strict mode, and schema composition.

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

CodeTrigger
invalid_typeInput is not a Map.
unknown_keyExtra key present in strict mode.
transform_errorobjectAs constructor threw.

API reference

MethodDescription
.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.

Next steps

Copyright © 2026