Zema Logo
Getting Started

Coming from Zod

A precise mapping between Zod (TypeScript) and Zema (Dart) APIs. Covers factory methods, constraint names, result handling, and type inference patterns.

Overview

Zema follows the same schema-first design as Zod. The factory object, fluent constraint API, and composability model are direct analogues. The differences arise from Dart's type system, runtime semantics, and the absence of TypeScript's structural typing.


Factory Methods

Zod (TypeScript)Zema (Dart)Notes
z.string()z.string()Identical.
z.number()z.integer() or z.double()Dart distinguishes int from double.
z.boolean()z.boolean()Identical.
z.date()z.dateTime()Returns DateTime. Accepts String, int (ms), or DateTime.
z.array(s)z.array(s)Identical signature.
z.object({})z.object({})Output type is Map<String, dynamic> in Zema.
z.union([...])z.union([...])Identical. Schemas tried in order.
z.literal(v)z.literal(v)Identical. Equality checked with ==.
z.record(v)z.map(k, v)Zema requires an explicit key schema.
z.enum(['a'])z.string().oneOf(['a'])No top-level enum factory. Use oneOf on ZemaString.
z.nativeEnum(E)No direct equivalent.Use z.string().oneOf(E.values.map((e) => e.name).toList()).
z.lazy(() => s)z.lazy(() => s)Identical. For self-referential schemas.
z.custom(fn)z.custom(fn)fn returns bool in Zema, not void.
z.preprocess(fn, s)s.preprocess(fn)Zema: method on the schema, not a top-level factory.

String Constraints

ZodZemaNotes
.min(n).min(n)Identical.
.max(n).max(n)Identical.
.email().email()Identical.
.url().url()Identical.
.uuid().uuid()Identical.
.regex(re)No public method.Use z.custom() or .refine() with a RegExp.
.trim().trim()Identical. Trimming occurs before length checks.
.toLowerCase().transform((s) => s.toLowerCase())No shorthand. Use transform.
.toUpperCase().transform((s) => s.toUpperCase())No shorthand. Use transform.

Number Constraints

Zod's z.number() maps to either z.integer() or z.double() in Zema depending on the required type. The constraint method names differ.

ZodZema (z.integer())Zema (z.double())
.min(n).gte(n).gte(n)
.max(n).lte(n).lte(n)
.positive().positive().positive()
.negative().negative()No equivalent: use .lte(-1).
.int()Use z.integer().N/A
.finite()No equivalent..finite()
.multipleOf(n).step(n)No equivalent.
// Zod
z.number().min(0).max(100).int()

// Zema
z.integer().gte(0).lte(100)

Modifiers

ZodZemaOutput type
.optional().optional()Output?
.nullable().nullable()Output?
.nullish().optional().nullable()Output?
.default(v).withDefault(v)Output — never null.
.catch(v).catchError((_) => v)Output — never fails.

Transformers

ZodZema
.transform(fn).transform(fn)
.pipe(schema).pipe(schema)
.preprocess(fn) on the schema.preprocess(fn)
// Zod
z.string().transform((s) => parseInt(s)).pipe(z.number().positive())

// Zema
z.string()
    .transform(int.parse)
    .pipe(z.integer().positive())

Refinements

ZodZema
.refine(fn, msg).refine(fn, message: msg)
.superRefine(fn).superRefine(fn)
.refine(async fn).refineAsync(fn)

Result Handling

Zod and Zema share the parse / safeParse split. The result shape differs.

Zod

const result = schema.safeParse(data);

if (result.success) {
  result.data; // typed value
} else {
  result.error.issues; // ZodIssue[]
}

Zema

final result = schema.safeParse(data);

switch (result) {
  case ZemaSuccess(:final value):
    // value is Output
  case ZemaFailure(:final errors):
    // errors is List<ZemaIssue>
}

Zema's ZemaResult is a sealed class. Pattern matching is exhaustive. There is no .error wrapper object; issues are on result.errors directly.


Type Inference

Zod: z.infer

type User = z.infer<typeof userSchema>;
// User = { id: number; email: string }

Zema: Extension Types

Dart does not support structural type inference from schema definitions. The equivalent pattern uses Extension Types, which wrap the validated Map<String, dynamic> with zero runtime cost.

final userSchema = z.object({
  'id':    z.integer(),
  'email': z.string().email(),
});

extension type User(Map<String, dynamic> _) {
  int get id => _['id'] as int;
  String get email => _['email'] as String;
}

// After validation:
if (result.isSuccess) {
  final user = result.value;
  print(user.email); // compile-time checked
}

At runtime, User is the Map<String, dynamic> itself. No allocation occurs.


Object Methods

ZodZema
.extend({}).extend({})
.pick({ key: true }).pick(['key'])
.omit({ key: true }).omit(['key'])
.strict().makeStrict()
.passthrough()Default behaviour (unknown keys are stripped, not rejected).
.merge(other).extend(other.shape)
// Zod
userSchema.pick({ email: true })

// Zema
userSchema.pick(['email'])

Error Customisation

Zod: message parameter

z.string().min(2, { message: 'Too short.' })

Zema: message named parameter

z.string().min(2, message: 'Too short.')
z.string().email(message: 'Invalid email.')

Global error map

Zod uses z.setErrorMap. Zema uses ZemaErrorMap.setErrorMap:

ZemaErrorMap.setErrorMap((issue) {
  if (issue.code == 'invalid_type') return 'Type invalide.';
  return null; // use default
});

Async Validation

ZodZema
schema.parseAsync(data)schema.parseAsync(data)
schema.safeParseAsync(data)schema.safeParseAsync(data)
.refine(async fn).refineAsync(fn)

Not Available in Zema

Zod featureStatus in Zema
z.tuple([...])No built-in. Use z.array() with index-based refinement.
z.discriminatedUnion('type', [...])No built-in. Use z.union([...]) with a preprocessing step.
z.function()No equivalent.
z.promise()No equivalent.
z.set(schema)No built-in. Use z.array(schema).transform((l) => l.toSet()).
schema.brand<T>()schema.brand<T>() — available via BrandedSchema.
Copyright © 2026