zodart 1.0.0-beta
zodart: ^1.0.0-beta copied to clipboard
Type-safe schema validation with static type inference and a parse-first design.
π― Type-safe schema validation with static type inference and a parse-first design for Dart and Flutter. #
Parse unstructured data from APIs, Flutter forms, config files, and more β with type safety and static type inference. ZodArt provides a powerful, expressive API to define validation schemas and parse unknown data into strongly typed Dart values. ZodArt never throws! You always get a typed value β or a detailed error report.
π ZodArt is under active development β feedback and contributions welcome!
Version | Status |
---|---|
current | |
next |
Simple example without code generation #
Even though ZodArt works perfectly without code generation, using it is highly recommended β it brings rock-solid type safety 𧬠and greatly improves DX π.
See Simple example with code generation to get started!
import 'package:zodart/zodart.dart';
/// The Person record type
typedef Person = ({String firstName, String lastName, int? age});
/// Schema defined using ZodArt
///
/// Validates that:
/// - `firstName` is from 1 to 20 characters long
/// - `lastName` is from 1 to 30 characters long
/// - `age` is β₯ 0 (optional)
final personSchema = ZObject<Person>.withMapper(
{
'firstName': ZString().min(1).max(20),
'lastName': ZString().min(1).max(30),
'age': ZInt().optional().min(0),
},
// Mapper used to construct the `Person` record from the parsed map
fromJson: (json) => (
firstName: json['firstName'],
lastName: json['lastName'],
age: json['age'],
),
);
void main() {
// Parse raw input (e.g. from an API, user form, etc.)
// ZodArt infers the type of `result.value` as `Person`
final result = personSchema.parse({'firstName': 'Zod', 'lastName': 'Art'});
// Option 1: Check success and access value
if (result.isSuccess) {
print(result.value); // (firstName: Zod, lastName: Art, age: null)
} else {
print(result.issueSummary); // Optional fallback
}
// Option 2: Use functional match to handle both cases
result.match(
(issues) => print('There was a problem: ${issues.localizedSummary}'),
(val) => print('Parsed person: $val'),
);
}
Table of Contents #
- Features
- Basic Usage
- Parsing values
- Nullable & optional values
- Validation & refine
- Value processing
- Localization & Custom Errors
- Additional information
Features #
- Rock-solid type safety with optional code generation (no more magic strings!)
- Define schemas for both primitive and complex types
- Parse unknown or unstructured data into strongly typed Dart values
- Seamless integration with
freezed
models - Composable and reusable schemas for easy code sharing and modularity
- Supports nested objects, arrays, optional and nullable fields
- Built-in various validation rules (e.g.
.min()
,.max()
, etc.) - Support for user-defined custom rules via
.refine()
- Rich, localizable, developer-friendly error messages
- Designed for use with REST APIs, GraphQL, JSON files, and form input
You can check the planned features and report bugs or feature requests by opening an issue on the GitHub page.
Basic usage #
Setup code generation #
Setting up ZodArt with code generation is quick and easy:
-
Add ZodArt and build_runner to your dependencies:
For Flutter project:
flutter pub add zodart flutter pub add dev:build_runner
For Dart project:
dart pub add zodart dart pub add dev:build_runner
-
Add a
part
directive to include the generated code:import 'package:zodart/zodart.dart'; // <your_file_name.zodart.dart> so for 'code_gen_example.dart' add: part 'code_gen_example.zodart.dart'; /// your code using `@zodart` annotation follows
-
Run
build_runner
to generate the ZodArt helpers classesdart run build_runner build
See more about code generation at build_runner package.
Simple example with code generation #
This is the same as the manual example, rewritten using code generation. See full example.
- β
Automatically generates a type-safe
ZObject
mapper β no more magic strings! - β Exposes type-safe property access to simplify field-specific issue handling
To instantiate a custom class (instead of a Record), use the withMapper
method provided in the generated utility class. See class example for more detail.
import 'package:zodart/zodart.dart';
// Make the generated code to be a part of this file
// You must change it to the name of `your_file_name.zodart.dart`
part 'code_gen_example.zodart.dart';
/// The Person record type used to store the result value
typedef Person = ({String firstName, String lastName, int? age});
/// Schema defined using ZodArt and autogenerated with [zodart] annotation
///
/// Generates helper class [_$ZPersonSchemaUtils]
///
/// Validates that:
/// - `firstName` is from 1 to 20 characters long
/// - `lastName` is from 1 to 30 characters long
/// - `age` is β₯ 0 (optional)
@zodart
abstract class PersonSchema {
/// Define the schema using Dart record.
static final schema = (
firstName: ZString().min(1).max(20),
lastName: ZString().min(1).max(30),
age: ZInt().optional().min(0),
);
/// Access to the generated helper class, which contains:
///
/// - The `ZObject` instance for parsing/validating the schema.
/// - A `withMapper` function for mapping parsed record to custom objects.
/// - A `shape` descriptor containing field mappings and runtime type info.
/// - Enum-style access to the schema properties.
static const z = _$ZPersonSchemaUtils();
/// Use the autogenerated [ZObject] with default mapper to [Person].
static ZObject<Person> get zObject => z.zObject;
}
void main() {
// Parse raw input (e.g. from an API, user form, etc.)
// ZodArt infers the type of `result.value` as `Person`
final result = PersonSchema.zObject.parse({'firstName': 'Zod', 'lastName': 'Art'});
// Option 1: Check success and access value
if (result.isSuccess) {
print(result.value); // (firstName: Zod, lastName: Art, age: null)
} else {
print(result.issueSummary); // Optional fallback
}
// Option 2: Use functional match to handle both cases
result.match(
(issues) => print('There was a problem: ${issues.localizedSummary}'),
(val) => print('Parsed person: $val'),
);
print(result.getRawIssuesFor(PersonSchema.z.props.firstName.name)?.localizedSummary);
}
Reusing and composing schemas #
Define simple schemas and compose them into complex ones. See full example.
import 'package:zodart/zodart.dart';
part 'zodart_compose_example.zodart.dart';
/// The String cannot be empty and is trimmed after the parse
final minSchema = ZString().trim().min(1);
/// Extends the [minSchema] and adds rule that the String must be max 10 characters long
final minMaxSchema = minSchema.max(10);
/// Extends the [minMaxSchema] and adds rule that the String can be `null`
final minMaxNullableSchema = minMaxSchema.nullable();
/// Extends the [minMaxNullableSchema] and conversion from String to Int in the end
final composedNullableIntSchema = minMaxNullableSchema.toInt();
/// Object schema composed from previously defined schemas, returns a Dart Record ({String str, int? intVal})
@zodart
abstract class ObjectSchema {
static final schema = (
str: minSchema,
intVal: composedNullableIntSchema,
);
static const z = _$ZObjectSchemaUtils();
static ZObject<({String str, int? intVal})> get zObj => z.zObject;
}
void main() {
// Returns: true (empty string after trim, violates min(1) rule)
minSchema.parse(' ').isError;
// Returns: 'ZodArt'
minSchema.parse(' ZodArt ').value;
// Returns: 'ZodArt'
minMaxSchema.parse(' ZodArt ').value;
// Returns: true
minMaxNullableSchema.parse(null).isSuccess;
// Returns: 105
composedNullableIntSchema.parse(' 105 ').value;
// Returns: true
composedNullableIntSchema.parse(null).isSuccess;
// Returns error message: Failed to parse value 'ZodArt', from String to int.
composedNullableIntSchema.parse('ZodArt').issueSummary;
// Returns: (intVal: 100, str: 'ZodArt')
ObjectSchema.zObj.parse({'intVal': ' 100 ', 'str': 'ZodArt'}).value;
}
Parsing values #
By default, ZodArt parsers operate in strict mode. This means they will only accept input values that match the expected type exactly. Any type mismatch will result in a ParseError.
The only exception is ZObject, which strictly accepts only Map<String, dynamic>
as input. See more parsers here.
Nullable & optional values #
In Dart, unlike JavaScript, there is no concept of undefined
value. However, when parsing ZObject
from Map<String, dynamic>
, a missing key (!map.containsKey('key')
) is the Dart equivalent of undefined
. To explicitly allow missing keys, ZodArt provides the .optional()
modifier.
For all other schemas like ZString
, ZInt
, etc., there is no concept of a "missing" value outside a ZObject
. In this context, the .optional()
modifier has no semantic effect and is treated as equivalent to .nullable()
.
See more at nullable modifier doc.
Validation & refine #
β οΈ Important: Do not throw exceptions inside a
.refine()
function β ZodArt will not catch them. βΉοΈ Validation is skipped automatically for null values.
Use the .refine()
method to add custom validation logic to any schema. This function should return true
if the value is valid, or false
otherwise.
When .refine()
returns false
, ZodArt creates a ZIssueCustom
issue.
You can optionally provide a message
or code
to include in the issue.
See full example.
βΉοΈ To return multiple issues or apply advanced validations use the
.superRefine()
method.
import 'package:zodart/zodart.dart';
part 'refine_example.zodart.dart';
/// The Person record type
typedef Person = ({String firstName, String lastName, DateTime validFrom, DateTime? validTo});
/// Schema defined using ZodArt
///
/// Validates that:
/// - `firstName` is from 1 to 20 characters long
/// - `lastName` is from 1 to 30 characters long
/// - `validFrom` is a DateTime
/// - `validTo` is an optional DateTime
@zodart
abstract class PersonSchema {
static final schema = (
firstName: ZString().min(1).max(20),
lastName: ZString().min(1).max(30),
validFrom: ZDateTime(),
validTo: ZDateTime().optional(),
);
static const z = _$ZPersonSchemaUtils();
// Refine the `schema` to ensure that `validFrom` β€ `validTo` or `validTo` is `null`
static ZObject<Person> get zObj => z.zObject.refine(
(person) {
final validTo = person.validTo;
return validTo == null || person.validFrom.isBefore(validTo);
},
message: 'validFrom must be earlier than validTo.',
);
}
void main() {
// Parse raw input (e.g. from an API, user form, etc.)
// ZodArt infers the type of `result.value` as `Person`
final result = PersonSchema.zObj.parse({
'firstName': 'Zod',
'lastName': 'Art',
'validFrom': DateTime(1900, 1, 1, 10, 01),
'validTo': DateTime(1900, 1, 1, 09, 01),
});
// Prints the custom error message 'validFrom must be earlier than validTo.'
print(result.issueSummary);
}
Value processing #
To transform a value, use the .process()
method.
It is available on all schema types, and can be chained freely.
The returned value must match the return type of the schema.
βΉοΈ Processing is skipped automatically for null values.
import 'package:zodart/zodart.dart';
String toTrendyUpperCase(String val) => 'π₯ ${val.trim().toUpperCase()}';
String toFlashySuffix(String val) => '$val β¨';
List<T> revertList<T>(List<T> val) => val.reversed.toList();
final zString = ZArray(ZString().process(toTrendyUpperCase).process(toFlashySuffix)).process(revertList);
void main() {
final res = zString.parse([' zodart ', 'world ', ' hello']);
print(res.value); // [π₯ HELLO β¨, π₯ WORLD β¨, π₯ ZODART β¨]
}
Localization & Custom Errors #
- Default language is set to English, to change it use
ZLocalizationContext.current
- ZodArt contains various helpers for error handling, see documentation for more info!
See full example.
import 'package:zodart/zodart.dart';
part 'localization_example.zodart.dart';
/// The Person record type
typedef Person = ({String firstName, String lastName, int? age});
/// Schema defined using ZodArt
///
/// Validates that:
/// - `firstName` is from 1 to 20 characters long
/// - `lastName` is from 1 to 30 characters long
/// - `age` is β₯ 0 (optional)
@zodart
abstract class PersonSchema {
static final schema = (
firstName: ZString().min(1).max(20),
lastName: ZString().min(1).max(30),
age: ZInt().optional().min(0),
);
static const z = _$ZPersonSchemaUtils();
static ZObject<Person> get zObj => z.zObject;
}
void main() {
final result = PersonSchema.zObj.parse({'firstName': '', 'lastName': 'Art', 'age': -1});
// Prints an English error message summary (default)
print(result.issueSummary);
// Change localization to Czech (or any supported language)
ZLocalizationContext.current = ZIssueLocalizationService(Language.cs);
// Prints error message summary in Czech
print(result.issueSummary);
// To get the individual localized message strings
final messages = result.issueMessages;
print(messages);
// Each issue is represented by a `ZIssue` instance
final zIssues = result.rawIssues;
// Custom translation logic using pattern matching (Dart 3+)
final customMessages =
zIssues?.map((zIssue) {
return switch (zIssue) {
ZIssueMinNotMet(:final min, :final val) => 'My custom message: $val is lower than $min!',
_ => 'My custom message: Other problem',
};
}) ??
[];
print('\nCustom messages:');
print(customMessages);
// To get error message summary only for the 'age' property
print('\nMessage for the age field:');
print(result.getSummaryFor('age'));
}
Additional information #
Explore the example/ folder β ready-to-use snippets you can copy into your project.