Zard
π‘οΈ Zard Documentation
Zard is a schema validation and transformation library for Dart, inspired by the popular Zod library for JavaScript. With Zard, you can define schemas to validate and transform data easily and intuitively.
Support π
If you find Zard useful, please consider supporting its development π Buy Me a Coffee π. Your support helps us improve the framework and make it even better!
Installation π¦
Add the following line to your pubspec.yaml:
dependencies:
zard: ^1.0.0
Then, run:
flutter pub get
Or run:
dart pub add zard
Usage π
Zard allows you to define schemas for various data types. Below are several examples of how to use Zard, including handling errors either by using parse (which throws errors) or safeParse (which returns a ZardResult).
Defining Schemas
String Example
import 'package:zard/zard.dart';
void main() {
// String validations with minimum length and email format check.
final schema = z.string().min(3).email();
// Using parse (throws ZardError on failure)
try {
final result = schema.parse('example@example.com');
print(result); // example@example.com
} on ZardError catch (e) {
print(e.issues.first.message);
}
// Using safeParse (never throws; returns ZardResult)
final result = schema.safeParse('hi'); // too short
if (!result.success) {
for (final issue in result.error!.issues) {
print('${issue.type}: ${issue.message}');
} else {
print(result.data);
}
}
}
β‘ Performance
| Library | Small Object | Complex Object |
|---|---|---|
| Zard | ~0.93 Β΅s | ~7.3 Β΅s |
| Zod | ~0.13 Β΅s | ~1.3 Β΅s |
| Yup | ~5.7 Β΅s | ~66 Β΅s |
Zard is:
- ~6-7x slower than Zod (JS engine advantage)
- ~6-10x faster than Yup
π Benchmark
Zard achieves:
- ~1M ops/sec (objects)
- ~4M ops/sec (primitives)
- ~100k ops/sec (complex nested)
Email validation
Zard added specific validations for emails using reusable patterns (RegExp). By default, z.string().email() validates using the HTML5 pattern (compatible with browser validation) which allows single-label domains (e.g.: john@example). It is possible to pass a pattern to choose a different behavior.
Available patterns in z.regexes:
html5Emailβ pattern used by browsers (allowsjohn@example).emailβ stricter, requires a TLD (e.g.:example.com).rfc5322Emailβ more complete implementation that follows the RFC 5322 specification (accepts quoted local-parts, tags, etc.).unicodeEmailβ permissive for non-ASCII characters (good for international emails), but simple.
Quick examples:
// 1) HTML5 pattern (browser default)
final html5 = z.string().email();
print(html5.parse('john@example')); // valid with html5Email
// 2) Force HTML5 pattern explicitly
final html5explicit = z.string().email(pattern: z.regexes.html5Email);
print(html5explicit.parse('john@example'));
// 3) Stricter pattern (requires TLD)
final strict = z.string().email(pattern: z.regexes.email);
print(strict.parse('john@example.com')); // valid
// strict.parse('john@example'); // throws error
// 4) RFC5322 (more complete)
final rfc = z.string().email(pattern: z.regexes.rfc5322Email);
print(rfc.parse('"john.doe"@example.co.uk')); // valid if it meets RFC
// 5) Unicode (accepts non-ASCII characters)
final uni = z.string().email(pattern: z.regexes.unicodeEmail);
print(uni.parse('usuΓ‘rio@exemplo.com'));
Use the pattern when you want to precisely control which email formats are accepted in your domain or application.
URL validation
Zard adds a convenient validator for URLs via z.string().url() with options to restrict hostname and protocol using custom RegExp.
Examples:
import 'package:zard/zard.dart';
void main() {
// 1) Default: accepts optional http(s) and generic hostname
final urlSchema = z.string().url();
print(urlSchema.parse('https://www.example.com'));
// 2) Force hostname ending with .example.com
final urlWithHostnameSchema =
z.string().url(hostname: RegExp(r'^[\w\.-]+\.example\.com$'));
print(urlWithHostnameSchema.parse('https://api.example.com/path'));
// 3) Force protocol (e.g.: only https)
final urlProtocolSchema = z.string().url(protocol: RegExp(r'^https:\/\/'));
print(urlProtocolSchema.parse('https://secure.example.com'));
// 4) Hostname + protocol customized simultaneously
final urlAllSchema = z.string().url(
hostname: RegExp(r'^[\w\.-]+\.example\.com$'),
protocol: RegExp(r'^https:\/\/'),
);
print(urlAllSchema.parse('https://api.example.com/endpoint'));
}
Important notes:
- You can pass
RegExptohostnameand/orprotocol. Patterns with anchors^and$are accepted β the validator removes these anchors internally when composing the final regex to avoid conflicts. - The case sensitivity (
isCaseSensitive) of theRegExpyou provide is respected; by default validation is case-insensitive when noRegExpspecifies otherwise. - The default behavior allows optional protocol (
http/https). To force a protocol, provide an appropriateRegExp(e.g.RegExp(r'^https:\/\/')).
String transforms (uppercase / lowercase / trim / normalize)
Zard adds convenient helpers and validators for common string operations:
uppercase()β validator: requires the value to already be all uppercase.lowercase()β validator: requires the value to already be all lowercase.toUpperCase()β transform: converts the value to uppercase.toLowerCase()β transform: converts the value to lowercase.trim()β transform: removes leading/trailing whitespace.normalize()β transform: removes accents/diacritics (usesstring_normalizerpackage), removes control characters, trims and collapses multiple whitespace into a single space.
Examples:
import 'package:zard/zard.dart';
void main() {
// 1) uppercase validator: accepts only strings already in UPPERCASE
final mustBeUpper = z.string().uppercase();
print(mustBeUpper.parse('ABC')); // ABC
// 2) lowercase validator: accepts only strings already in lowercase
final mustBeLower = z.string().lowercase();
print(mustBeLower.parse('abc')); // abc
// 3) Transform toUpperCase / toLowerCase
print(z.string().toUpperCase().parse('hello')); // HELLO
print(z.string().toLowerCase().parse('HELLO')); // hello
// 4) Trim
print(z.string().trim().parse(' hello ')); // hello
// 5) Normalize (removes accents/diacritics, trim, collapse whitespace)
print(z.string().normalize().parse(' Ñéà ')); // aei
}
String β Boolean (stringbool)
Zard provides a convenient schema to interpret strings as booleans via z.stringbool().
It accepts boolean, numeric, and string values that represent true or false states.
Recognized tokens (case-insensitive, with trim):
- True:
1,true,yes,on,y,enabled - False:
0,false,no,off,n,disabled
Examples:
import 'package:zard/zard.dart';
void main() {
final strbool = z.stringbool();
print(strbool.parse('1')); // true
print(strbool.parse('yes')); // true
print(strbool.parse('ON')); // true
print(strbool.parse(' enabled ')); // true (trim + case-insensitive)
print(strbool.parse('0')); // false
print(strbool.parse('no')); // false
print(strbool.parse(true)); // true
print(strbool.parse(0)); // false
// Unrecognized values throw ZardError
// strbool.parse('maybe'); // throws ZardError
}
Advanced String Validators
Zard provides a series of specialized validators for common string types (URLs, IPs, hashes, etc.):
Identifiers and UUIDs:
guid()β GUID/UUID v4uuid(version)β Generic UUID (v1-v8) or specific versionnanoid()β Nano ID (21 characters)ulid()β ULID (Universally Unique Lexicographically Sortable Identifier)
Networks and Protocols:
httpUrl()β HTTP/HTTPS URLs onlyhostname()β Valid hostnameipv4()β IPv4 addressipv6()β IPv6 addressmac()β MAC address (e.g.:AA:BB:CC:DD:EE:FF)cidrv4()β IPv4 CIDR block (e.g.:192.168.1.0/24)cidrv6()β IPv6 CIDR block
Encodings and Hashes:
base64()β Standard Base64base64url()β Base64 URL-safehex()β Hexadecimalhash(algorithm)β Hash validated by algorithm (supportssha1,sha256,sha384,sha512,md5)jwt()β JSON Web Token
Other Formats:
emoji()β A single emoji character
Examples:
import 'package:zard/zard.dart';
void main() {
z.string().guid().parse('550e8400-e29b-41d4-a716-446655440000');
z.string().uuid(version: 'v4').parse('550e8400-e29b-41d4-a716-446655440000');
z.string().nanoid().parse('V1StGXR_Z5j3eK4CFLQ');
z.string().ulid().parse('01ARZ3NDEKTSV4RRFFQ69G5FAV');
z.string().httpUrl().parse('https://example.com');
z.string().ipv4().parse('192.168.1.1');
z.string().ipv6().parse('2001:0db8:85a3:0000:0000:8a2e:0370:7334');
z.string().mac().parse('AA:BB:CC:DD:EE:FF');
z.string().base64().parse('SGVsbG8gV29ybGQ=');
z.string().hex().parse('48656C6C6F');
z.string().hash('sha256').parse('e3b0c44298fc1c149afbf4c8996fb924...');
z.string().jwt().parse('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...');
z.string().emoji().parse('π');
}
ISO 8601 Date/Time Validators
Zard provides specialized validators for ISO 8601 formats, accessible via the z.iso.* namespace:
z.iso.date()β ISO Date (YYYY-MM-DD)z.iso.time()β ISO Time (HH:mm:ss or with milliseconds)z.iso.datetime()β ISO 8601 Date and Time (with or without Z)z.iso.duration()β ISO 8601 Duration (e.g.: P1DT2H3M4S)
Examples:
import 'package:zard/zard.dart';
void main() {
print(z.iso.date().parse('2021-01-01')); // valid
print(z.iso.time().parse('12:30:45')); // valid
print(z.iso.datetime().parse('2021-01-01T12:30:45Z')); // valid
print(z.iso.duration().parse('P1Y2M3DT4H5M6S')); // valid
}
Int Example
import 'package:zard/zard.dart';
void main() {
final schema = z.int().min(1).max(100);
print(schema.parse(50)); // 50
final result = schema.safeParse(0); // below min
if (!result.success) {
print(result.error!.issues.first.message);
}
}
Double Example
import 'package:zard/zard.dart';
void main() {
final schema = z.double().min(1.0).max(100.0);
print(schema.parse(50.5)); // 50.5
final result = schema.safeParse(0.5);
if (!result.success) {
print(result.error!.issues.first.message);
}
}
Boolean Example
import 'package:zard/zard.dart';
void main() {
final schema = z.bool();
print(schema.parse(true)); // true
final result = schema.safeParse('yes'); // wrong type
print(result.success); // false
}
List Example
import 'package:zard/zard.dart';
void main() {
final schema = z.list(z.string().min(3));
print(schema.parse(['abc', 'def'])); // [abc, def]
final result = schema.safeParse(['ab', 'def']); // 'ab' too short
if (!result.success) {
for (final issue in result.error!.issues) {
print('${issue.path}: ${issue.message}');
}
}
}
Map / Object Example
import 'package:zard/zard.dart';
void main() {
final schema = z.map({
'name': z.string().min(3),
'age': z.int().min(0),
'email': z.string().email(),
}).refine(
(value) => value['age'] > 18,
message: 'Age must be greater than 18',
);
final result = schema.safeParse({
'name': 'John Doe',
'age': 20,
'email': 'john.doe@example.com',
});
print(result.success); // true
print(result.data);
final result2 = schema.safeParse({
'name': 'John Doe',
'age': 10,
'email': 'john.doe@example.com',
});
print(result2.success); // false
print(result2.error!.issues.first.message); // Age must be greater than 18
}
Object Utility Methods
ZMap (and ZInterface) schemas support Zod-parity utility methods for transforming the schema shape:
import 'package:zard/zard.dart';
void main() {
final userSchema = z.map({
'name': z.string(),
'age': z.int(),
'email': z.string().email().optional(),
});
// partial(): all (or specific) fields become optional
final partialUser = userSchema.partial();
print(partialUser.parse({'name': 'Alice'})); // {name: Alice}
final partialAge = userSchema.partial(keys: ['age']);
print(partialAge.parse({'name': 'Bob', 'email': 'bob@x.com'}));
// required(): all (or specific) optional fields become required
final requiredUser = userSchema.required();
// merge(): combine two schemas (second wins on conflicts)
final withRole = userSchema.merge(z.map({'role': z.string()}));
print(withRole.parse({'name': 'Carol', 'age': 30, 'role': 'admin'}));
// extend(): add extra fields
final extended = userSchema.extend({'phone': z.string().optional()});
// pick(): keep only named fields
final nameOnly = userSchema.pick(['name']);
print(nameOnly.parse({'name': 'Dave'}));
// omit(): remove named fields
final noEmail = userSchema.omit(['email']);
// keyof(): enum schema of the schema's keys
final keys = userSchema.keyof();
print(keys.parse('name')); // name
print(keys.safeParse('unknown').success); // false
}
Date Example
import 'package:zard/zard.dart';
void main() {
final schema = z.date();
print(schema.parse(DateTime.now()).year); // current year
final result = schema.safeParse('2025-11-26');
if (result.success) {
print(result.data); // DateTime instance
}
}
Enum Example
import 'package:zard/zard.dart';
void main() {
final schema = z.$enum(['pending', 'active', 'inactive']);
print(schema.parse('active')); // active
final result = schema.safeParse('unknown');
print(result.success); // false
// Extract or exclude values
final active = schema.extract(['active', 'pending']);
final noInactive = schema.exclude(['inactive']);
}
Default Value Example
import 'package:zard/zard.dart';
void main() {
final schema = z.map({
'name': z.string(),
'status': z.string().$default('active'),
'age': z.int().$default(18),
});
// Absent fields use their defaults
print(schema.parse({'name': 'John'}));
// {name: John, status: active, age: 18}
// Present-null fields also get the default
print(schema.parse({'name': 'Jane', 'status': null, 'age': null}));
// {name: Jane, status: active, age: 18}
}
Coerce Example
import 'package:zard/zard.dart';
void main() {
print(z.coerce.int().parse('123')); // 123
print(z.coerce.double().parse('3.14')); // 3.14
print(z.coerce.bool().parse('true')); // true
print(z.coerce.string().parse(123)); // "123"
print(z.coerce.date().parse('2025-11-26')); // DateTime
}
Lazy Schema Example (Circular References)
import 'package:zard/zard.dart';
void main() {
// Recursive schema for a tree structure
late Schema<Map<String, dynamic>> nodeSchema;
nodeSchema = z.map({
'value': z.string(),
'children': z.list(z.lazy(() => nodeSchema)).optional(),
});
final tree = nodeSchema.parse({
'value': 'root',
'children': [
{'value': 'child1'},
{
'value': 'child2',
'children': [
{'value': 'grandchild'},
],
},
],
});
print(tree['value']); // root
print((tree['children'] as List).length); // 2
}
Advanced Features π―
Transform Values
import 'package:zard/zard.dart';
void main() {
// transform(): same output type
final upper = z.string().transform((s) => s.toUpperCase());
print(upper.parse('hello')); // HELLO
// transformTyped(): change output type
final length = z.string().transformTyped<int>((s) => s.length);
print(length.parse('hello')); // 5
// Chain transforms on object fields
final schema = z.map({
'email': z.string().email().transform((v) => v.toLowerCase()),
'name': z.string().transform((v) => v.toUpperCase()),
});
print(schema.parse({'email': 'JOHN@X.COM', 'name': 'john'}));
// {email: john@x.com, name: JOHN}
}
Optional and Nullable Fields
import 'package:zard/zard.dart';
void main() {
final schema = z.map({
'name': z.string(),
'nickname': z.string().optional(), // key may be absent
'middleName': z.string().nullable(), // value may be null (key must be present)
'age': z.int().nullish(), // key may be absent OR value may be null
});
final result = schema.safeParse({
'name': 'John Doe',
'middleName': null,
'age': null,
});
if (result.success) {
print(result.data); // {name: John Doe, middleName: null, age: null}
}
}
Strict Mode
import 'package:zard/zard.dart';
void main() {
final schema = z.map({
'name': z.string(),
'email': z.string().email(),
}).strict(); // reject unknown keys
final result = schema.safeParse({
'name': 'John Doe',
'email': 'john@example.com',
'phone': '123-456-7890', // not in schema
});
print(result.success); // false
print(result.error!.issues.first.message); // Unexpected key "phone" found in object
}
Refine (Custom Validation)
import 'package:zard/zard.dart';
void main() {
// Single-field refine
final schema = z.int().refine((n) => n % 2 == 0, message: 'Must be even');
print(schema.parse(4)); // 4
print(schema.safeParse(3).success); // false
// Cross-field refine on a map
final passwords = z
.map({'password': z.string().min(8), 'confirm': z.string()})
.refine(
(data) => data['password'] == data['confirm'],
message: 'Passwords must match',
);
final result = passwords.safeParse({
'password': 'secret123',
'confirm': 'different',
});
print(result.error!.issues.first.message); // Passwords must match
}
ZardResult API
safeParse() and safeParseAsync() return a ZardResult<T> with the following interface:
| Member | Description |
|---|---|
result.success |
true if parsing succeeded |
result.data |
The parsed value (non-null when success is true) |
result.error |
The ZardError (non-null when success is false) |
result.unwrap() |
Returns data or throws ZardError |
result.unwrapOrNull() |
Returns data or null β never throws |
result.when(success:, error:) |
Pattern-match on success/failure |
import 'package:zard/zard.dart';
void main() {
final schema = z.map({
'name': z.string().min(2),
'age': z.int().min(0),
});
final result = schema.safeParse({'name': 'A', 'age': -1});
// unwrap β throws on failure
try {
final data = result.unwrap();
} on ZardError catch (e) {
print('Failed: ${e.issues.length} issues');
}
// unwrapOrNull β null on failure
print(result.unwrapOrNull()); // null
// when β pattern match
result.when(
success: (data) => print('ok: $data'),
error: (err) => print('fail: ${err.issues.first.message}'),
);
}
Error Formatting
Zard exposes three error-formatting helpers on the z object:
z.flattenError(error)
Collapses all issues into a flat {formErrors, fieldErrors} structure. fieldErrors keys are the top-level field path segments.
final flattened = z.flattenError(result.error!);
print(flattened.formErrors); // root-level errors
print(flattened.fieldErrors); // {'name': [...], 'age': [...]}
// firstErrors: one message per field (handy for form hints)
print(flattened.firstErrors); // {'name': 'Value must be at least 2 characters long', ...}
z.treeifyError(error)
Builds a nested tree reflecting the path structure of the issues.
final tree = z.treeifyError(result.error!);
print(tree.errors); // root-level error messages
print(tree.properties?['name']?.errors); // field-level messages
print(tree.items?[0]?.errors); // list-item-level messages
z.prettifyError(error)
Returns a human-readable multi-line string.
print(z.prettifyError(result.error!));
// β Value must be at least 2 characters long
// β at name
// β Value must be at least 0
// β at age
Async Validation
import 'package:zard/zard.dart';
void main() async {
final schema = z.string().min(3);
// parseAsync accepts a plain value or a Future
final value = await schema.parseAsync(Future.value('hello'));
print(value); // hello
// safeParseAsync returns a Future<ZardResult<T>>
final result = await schema.safeParseAsync(Future.value('hi'));
print(result.success); // false
}
inferType
z.inferType combines a ZMap schema with a factory function, returning a typed schema that parses and converts in one step.
import 'package:zard/zard.dart';
class User {
final String name;
final int age;
User({required this.name, required this.age});
}
void main() {
final schema = z.map({'name': z.string(), 'age': z.int()});
final userType = z.inferType<User>(
fromMap: (m) => User(name: m['name'] as String, age: m['age'] as int),
mapSchema: schema,
);
final user = userType.parse({'name': 'Alice', 'age': 30});
print(user.name); // Alice
}
Error Handling with ZardError π΅βπ«
When a validation fails, Zard throws ZardError. Each ZardIssue inside it contains:
messageβ a descriptive message about what went wrong.typeβ the error type (e.g.,min_error,max_error,type_error,required_error).valueβ the value that failed validation.pathβ dot-notation path to the failing field (e.g.,address.ziporitems[0].name).
Two parsing methods:
parse()β throwsZardErroron failure.safeParse()β returnsZardResult<T>; never throws.
Similarity to Zod
Zard was inspired by Zod, a powerful schema validation library for JavaScript. Just like Zod, Zard provides an easy-to-use API for defining and transforming schemas. The main difference is that Zard is built specifically for Dart and Flutter, harnessing the power of Dart's language features.
Contribution
Contributions are welcome! Feel free to open issues and pull requests on the GitHub repository.
License
This project is licensed under the MIT License. See the LICENSE file for more details.
Made with β€οΈ for Dart/Flutter developers! π―β¨
Contributors
```Libraries
- zard
- More dartdocs go here.