zema 0.5.1 copy "zema: ^0.5.1" to clipboard
zema: ^0.5.1 copied to clipboard

A high-performance, type-safe schema validation library for Dart & Flutter.

Zema #

pub package package publisher License: MIT

Schema validation for Dart, inspired by Zod. Define schemas once, validate anywhere. All validation errors are collected in a single pass: no silent failures, no partial results.


Features #

  • Fluent, chainable API: z.string().min(2).email()
  • Exhaustive error collection: every failing field is reported, not just the first
  • Sealed result type: ZemaSuccess<T> and ZemaFailure<T>, no exceptions by default
  • Composable objects: extend(), merge(), pick(), omit()
  • Discriminated unions with O(1) schema selection
  • Async refinements for database and network checks
  • Coercion for environment variables and query parameters
  • Nominal branding for type-safe IDs
  • Built-in i18n with en and fr locales; extensible to any locale

Installation #

dependencies:
  zema: ^0.3.0
import 'package:zema/zema.dart';

Quick start #

final userSchema = z.object({
  'name': z.string().min(2),
  'email': z.string().email(),
  'age': z.integer().gte(18).optional(),
});

// parse() returns the validated value or throws ZemaException
final user = userSchema.parse({
  'name': 'Alice',
  'email': 'alice@example.com',
});

// safeParse() never throws, returns ZemaResult<T>
final result = userSchema.safeParse(rawInput);

switch (result) {
  case ZemaSuccess(:final value):
    print(value['name']);
  case ZemaFailure(:final errors):
    for (final issue in errors) {
      print('${issue.path.join(".")}: ${issue.message}');
    }
}

Primitives #

z.string()         // String
z.integer()        // int
z.double()         // double
z.boolean()        // bool
z.dateTime()       // DateTime
z.literal('admin') // exact value

String constraints #

z.string().min(2).max(100)
z.string().email()
z.string().url()
z.string().uuid()
z.string().regex(RegExp(r'^\d{5}$'))
z.string().trim().min(1)
z.string().oneOf(['draft', 'published', 'archived'])

Number constraints #

z.integer().gte(0).lte(120)
z.integer().positive()      // > 0
z.integer().negative()      // < 0
z.integer().nonNegative()   // >= 0
z.integer().step(5)         // multiple of 5

z.double().gte(0.0).lte(1.0)
z.double().nonNegative()
z.double().finite()

Objects #

final schema = z.object({
  'id': z.string().uuid(),
  'name': z.string().min(2),
  'email': z.string().email(),
  'password': z.string().min(8),
});

// Reject unknown keys
final strict = schema.makeStrict();

// Add fields
final extended = schema.extend({'role': z.string()});

// Merge two schemas (fields from other win on conflict)
final merged = base.merge(override);

// Subset of fields
final public = schema.pick(['id', 'name', 'email']);
final safe = schema.omit(['password']);

Typed output #

final schema = z.objectAs(
  {'name': z.string(), 'age': z.integer()},
  (map) => User(name: map['name'] as String, age: map['age'] as int),
);

final User user = schema.parse(data);

Arrays #

z.array(z.string())
z.array(z.integer()).min(1).max(100)
z.array(z.string().email()).nonEmpty()
z.array(z.string()).length(3)

// All element errors collected in one pass, with index in error path
z.array(z.object({'email': z.string().email()}))

Unions #

// Linear scan: first matching schema wins
final id = z.union<dynamic>([
  z.string().uuid(),
  z.integer().positive(),
]);

// Discriminated union: O(1) lookup via literal field
final event = z.union([
  z.object({'type': z.literal('click'),    'x': z.integer(), 'y': z.integer()}),
  z.object({'type': z.literal('keypress'), 'key': z.string()}),
]).discriminatedBy('type');

event.parse({'type': 'click', 'x': 100, 'y': 200});

Modifiers #

z.string().optional()             // String? (null passes through)
z.string().nullable()             // String? (null is a valid value)
z.integer().withDefault(0)        // always returns a value, never null
z.integer().catchError((_) => 0)  // intercept failures, inspect issues

// Nominal branding: prevent mixing semantically different IDs
abstract class _UserIdBrand {}
abstract class _TeamIdBrand {}

final userIdSchema = z.string().uuid().brand<_UserIdBrand>();
final teamIdSchema = z.string().uuid().brand<_TeamIdBrand>();

final userId = userIdSchema.parse('550e8400-…'); // Branded<String, _UserIdBrand>
// greet(teamId); // compile-time error

Transformations #

// .transform() changes the output type
final schema = z.string().transform(int.parse); // output: int

// .pipe() passes one schema's output into another
final piped = z.string().pipe(z.integer());

// .preprocess() normalises raw input before validation
final schema = z.preprocess(
  (v) => v.toString().trim(),
  z.string().email(),
);

Refinements #

// Single boolean check
z.string().refine(
  (s) => s.startsWith('https'),
  message: 'Must use HTTPS.',
);

// Multiple issues from one check
z.string().superRefine((s, ctx) {
  final issues = <ZemaIssue>[];
  if (!s.contains(RegExp(r'[A-Z]'))) {
    issues.add(ZemaIssue(code: 'missing_uppercase', message: 'Needs an uppercase letter.'));
  }
  if (!s.contains(RegExp(r'[0-9]'))) {
    issues.add(ZemaIssue(code: 'missing_digit', message: 'Needs a digit.'));
  }
  return issues.isEmpty ? null : issues;
});

// Async check (database lookup, API call)
final schema = z.string().email().refineAsync(
  (email) async => !(await db.emailExists(email)),
  message: 'Email already taken.',
);

final result = await schema.safeParseAsync(input);

Coercion #

Parse strings from environment variables, query parameters, or form inputs:

z.coerce().integer()  // '42' → 42
z.coerce().double()   // '3.14' → 3.14
z.coerce().boolean()  // 'true' | '1' → true
z.coerce().string()   // any → String

Error handling #

// safeParse: sealed result, never throws
final result = schema.safeParse(input);

if (result.isFailure) {
  for (final issue in result.errors) {
    // issue.code: machine-readable string ('invalid_type', 'too_short', …)
    // issue.message: human-readable string
    // issue.path: location in the input (['user', 'email'])
    print('[${issue.code}] ${issue.path.join(".")}: ${issue.message}');
  }
}

// parse: throws ZemaException on failure
try {
  final value = schema.parse(input);
} on ZemaException catch (e) {
  print(e.issues.map((i) => i.message).join(', '));
}

i18n #

// Built-in locales: 'en' (default), 'fr'
ZemaErrorMap.setLocale('fr');

// Custom locale
ZemaI18n.registerTranslations('es', {
  'invalid_type': 'Tipo inválido: se esperaba {expected}, se recibió {received}.',
  'too_short': 'Demasiado corto: mínimo {min}.',
  'too_long': 'Demasiado largo: máximo {max}.',
  'invalid_email': 'Dirección de correo inválida.',
});
ZemaErrorMap.setLocale('es');

// Custom error map for per-schema overrides
ZemaErrorMap.setErrorMap((code, ctx) {
  if (code == 'invalid_type' && ctx?['expected'] == 'string') {
    return 'A text value is required.';
  }
  return null; // fall back to locale default
});

Custom schemas #

final tokenSchema = z.custom<String>(
  (value) => value is String && value.startsWith('tok_'),
  message: 'Must be a valid token.',
);

Documentation #

Full guides, API reference, and examples: zema.meragix.dev


License #

MIT License — see LICENSE

1
likes
160
points
246
downloads

Documentation

Documentation
API reference

Publisher

verified publishermeragix.dev

Weekly Downloads

A high-performance, type-safe schema validation library for Dart & Flutter.

Homepage
Repository (GitHub)
View/report issues
Contributing

Topics

#validation #schema #parsing #zod #type-safe

License

MIT (license)

Dependencies

meta

More

Packages that depend on zema