Coming from Zod
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
| Zod | Zema | Notes |
|---|---|---|
.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.
| Zod | Zema (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
| Zod | Zema | Output 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
| Zod | Zema |
|---|---|
.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
| Zod | Zema |
|---|---|
.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
| Zod | Zema |
|---|---|
.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
| Zod | Zema |
|---|---|
schema.parseAsync(data) | schema.parseAsync(data) |
schema.safeParseAsync(data) | schema.safeParseAsync(data) |
.refine(async fn) | .refineAsync(fn) |
Not Available in Zema
| Zod feature | Status 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. |