eskema 1.0.0
eskema: ^1.0.0 copied to clipboard
Eskema is a small, composable runtime validation library for Dart. It helps you validate dynamic values (JSON, Maps, Lists, primitives) with readable validators and clear error messages.

Eskema #
Eskema is a small, composable runtime validation library for Dart. It helps you validate dynamic values (JSON, Maps, Lists, primitives) with readable validators and clear error messages.
Use cases #
Here are some common usecases for Eskema:
- Validate untyped API JSON before mapping to models (catch missing/invalid fields early).
- Guard inbound request payloads (HTTP handlers, jobs) with clear, fail-fast errors.
- Validate runtime config and feature flags from files or remote sources.
Install #
dart pub add eskema
Quick start #
Validate a map using a schema-like validator and get back a detailed result.
1. Create a validator #
import 'package:eskema/eskema.dart';
final userValidator = eskema({
// Use built-in validator functions
'username': isString() & isNotEmpty(),
// Some zero-arg validators also have cached aliases (e.g. $isBool, $isString)
'lastname': $isString,
// Combine validators using operators for simplicity and readability
'age': isInt() & isGte(0),
'theme': isString() & isIn(['light', 'dark']),
// The key must exist, but the value may be null.
'bio': nullable(isString()),
// The key may be missing entirely. If present, it must be a valid DateTime string.
'birthday': optional(isDateTimeString()),
});
2. Validate your data #
Use the .validate() method to get a Result object, which contains the validation status, errors, and the original value.
final result = userValidator.validate({
'username': 'alice',
'lastname': 'smith',
'age': -1, // Invalid
'theme': 'system', // Invalid
'bio': null, // Valid
// 'birthday' is missing, which is valid for an optional field
});
if (!result.isValid) {
print(result);
// Result(
// isValid: false,
// value: { ... },
// expectations: [
// age: must be greater than or equal to 0,
// theme: must be one of [light, dark]
// ]
// )
}
You can also get a simple boolean or have it throw an exception on failure.
// Get a boolean result
final ok = userValidator.isValid({'username': 'bob', 'lastname': 'p', 'age': 42, 'theme': 'light'});
print("User is valid: $ok"); // true
// Throw an exception on failure
try {
userValidator.validateOrThrow({'username': 'bob'});
} catch (e) {
print(e); // ValidatorFailedException with a helpful message
}
Table of contents #
- Eskema
API overview #
Check the docs for the full technical documentation.
Need machine readable errors? See Expectation Codes & Data for the mapping between validators, codes and data payload.
-
Core
IValidator— The base class for all validators.Result— The output of a validation, containing.isValid,.expectations, and.value.eskema({ 'key': validator, ... })— Validates maps against a schema.eskemaStrict({ 'key': validator, ... })— Likeeskema, but fails on unknown keys.
-
Common Validators
- Types:
isString(),isInt(),isDouble(),isBool(),isList(),isMap(),isDateTime() - Presence:
isNull(),isNotNull(),isNotEmpty(),isPresent() - Composition:
&(AND),|(OR),not() - Comparison:
isGt(n),isGte(n),isLt(n),isLte(n),isEq(v),isDeepEq(v),isInRange(min, max) - Strings:
hasLength(n),contains(s),startsWith(s),endsWith(s),matchesPattern(re),isEmail(),isUrl(),isUuid(),isDateTimeString() - Lists:
listEach(v),listIsOfLength(n),contains(v)
- Types:
-
Modifiers
v.nullable()— Allows the value to benull(key must be present).v.optional()— Allows the key to be missing.v > 'custom error'— Overrides the default error message.
-
Results & Helpers
.validate(value)→Result.validateAsync(value)→Future<Result>(use when any validator is async).isValid(value)→bool.validateOrThrow(value)→ throwsValidatorFailedExceptionon invalid input.AsyncValidatorException— thrown if you call.validate()on a chain that contains async validators.
Async validation #
Eskema supports mixing synchronous and asynchronous validators without forcing everything to become async. Validators internally return FutureOr<Result> and only upgrade to a Future when an async boundary is encountered.
Key points:
- Use
.validate()for purely synchronous validator chains (fast path, no allocations for Futures). - If any validator in the chain is async (uses
async/ returns aFuture<Result>), call.validateAsync()instead. - Calling
.validate()on a chain that resolves an async validator throwsAsyncValidatorExceptionwith a helpful message. - Synchronous and async validators compose seamlessly; you do not need separate APIs for "async versions" of built-ins.
Creating an async validator #
You can make any custom validator async simply by returning a Future<Result> (e.g. using async). For example, checking a username against an in‑memory or remote store:
// Simulated async uniqueness check
final $isUsernameAvailable = Validator((value) async {
await Future<void>.delayed(const Duration(milliseconds: 10));
const taken = {'alice', 'root'};
if (value is String && !taken.contains(value)) {
return Result.valid(value);
}
return Result.invalid(value, expectation: Expectation(message: 'not available', value: value));
});
final userValidator = eskema({
'username': isString() & isNotEmpty() & $isUsernameAvailable,
'age': isInt() & isGte(0),
});
// Because one link is async, use validateAsync()
final r = await userValidator.validateAsync({'username': 'new_user', 'age': 30});
print(r.isValid); // true
// Calling validate() here would throw AsyncValidatorException
Mixing sync & async combinators #
Combinators like all, any, none, not, schema validators (eskema, eskemaStrict, eskemaList, listEach) and when all propagate async seamlessly. They stay synchronous until a child returns a Future and only then switch to async.
When to prefer validateAsync() #
- Any time you intentionally include an async validator.
- If you want a uniform
Futureinterface regardless of sync/async (e.g. in higher‑level code). It is safe but you lose the micro‑optimization of the sync fast path.
Error handling #
- Use
validateAsync()+ checkr.isValid. - Or, wrap an async chain with
throwInstead(v)and handleValidatorFailedExceptioninside atry/catch. - Misuse (calling
.validate()on async chain) →AsyncValidatorException.
Upgrading existing code #
Most existing synchronous validators require no changes. Only update call sites to .validateAsync() where you introduce an async validator.
Transformers #
Transformers coerce or modify a value before it's passed to a child validator. This is useful for converting strings to numbers, trimming whitespace, or providing default values.
// Coerce a string to an integer, then validate the number
final ageValidator = toInt(isInt() & isGte(18));
ageValidator.validate('25'); // Valid, value becomes 25
ageValidator.validate('invalid'); // Invalid
// Provide a default value for a missing or null field
final settingsValidator = eskema({
'theme': defaultTo('light', isIn(['light', 'dark'])),
});
settingsValidator.validate({}); // Valid, theme becomes 'light'
// Split a string into a list and validate each item
final tagsValidator = split(',', listEach(isString() & isNotEmpty()));
tagsValidator.validate('dart,flutter,eskema'); // Valid
Available transformers:
toInt(child)toDouble(child)toNum(child)toBool(child)toDateTime(child)trim(child)toLowerCase(child)toUpperCase(child)defaultTo(defaultValue, child)split(separator, child)getField(key, child)
Conditional Validation #
The when validator allows you to apply different validation rules based on the value of another field in the same map.
final addressValidator = eskema({
'country': isIn(['USA', 'Canada']),
'postal_code': when(
// Condition (on the parent map)
getField('country', isEq('USA')),
// `then` validator (for the `postal_code` field)
then: isString() & hasLength(5) > 'a 5-digit US zip code',
// `otherwise` validator (for the `postal_code` field)
otherwise: isString() & hasLength(6) > 'a 6-character Canadian postal code',
),
});
// This is valid
addressValidator.validate({
'country': 'USA',
'postal_code': '90210',
});
// This is also valid
addressValidator.validate({
'country': 'Canada',
'postal_code': 'M5H2N2',
});
// This is invalid
addressValidator.validate({
'country': 'USA',
'postal_code': 'M5H2N2',
});
Examples #
Custom validators #
You can create your own validators by composing existing ones or by creating a new Validator instance.
// 1. By composition
IValidator isPositive() => isInt() & isGte(0);
// 2. With the `validator` helper
IValidator isDivisibleBy(int n) {
return validator(
(value) => value is int && value % n == 0,
(value) => Expectation(message: 'must be divisible by $n', value: value),
);
}
// 3. With a custom class (for more complex logic)
class MyCustomValidator extends Validator {
MyCustomValidator() : super((value) {
if (value == 'magic') {
return Result.valid(value);
}
return Result.invalid(value, expectations: [Expectation(message: 'not magic', value: value)]);
});
}
Nullable vs optional #
The distinction between nullable and optional is important for map validation.
nullable(): The key must be present in the map, but its value can benull.optional(): The key may be missing from the map. If it is present, its value must not benull(unlessnullableis also used).
final validator = eskema({
'required_but_nullable': isString().nullable(),
'optional_and_not_nullable': isString().optional(),
'optional_and_nullable': isString().nullable().optional(),
});
// Key must exist, value can be null
validator.validate({ 'required_but_nullable': null }); // Valid
validator.validate({}); // Invalid: 'required_but_nullable' is missing
// Key can be missing. If present, value cannot be null.
validator.validate({ 'optional_and_not_nullable': 'hello' }); // Valid
validator.validate({}); // Valid
validator.validate({ 'optional_and_not_nullable': null }); // Invalid
// Key can be missing. If present, value can be null.
validator.validate({ 'optional_and_nullable': null }); // Valid
validator.validate({}); // Valid
Expectation codes #
Eskema returns a list of Expectation objects on failure. Each carries:
message– Human friendly descriptioncode– Namespaced identifier (e.g.type.mismatch,value.range_out_of_bounds)path– Location within the validated structure (e.g..user.address[0].city)data– Structured metadata (e.g.{ "expected": "String", "found": "int" })
The full table of built‑in validators → codes lives in docs/expectation_codes.md. Use it to localize, categorize, or branch logic on specific error types. Codes are additive and stable (changes are breaking only in a major release). Always ignore unknown future codes for forward compatibility.
Contributing #
Contributions are welcome! Whether you've found a bug, have a feature request, or want to contribute code, please feel free to open an issue or a pull request.
Reporting Bugs #
If you find a bug, please open an issue on the GitHub repository. Include a clear description of the problem, steps to reproduce it, and the expected behavior.
Feature Requests #
If you have an idea for a new feature or an improvement to an existing one, please open an issue to start a discussion. This allows us to align on the feature before any code is written.
Pull Requests #
- Fork the repository and create your branch from
main. - Install dependencies:
dart pub get - Make your changes. Please add tests for any new features or bug fixes.
- Run tests:
dart test - Ensure your code is formatted:
dart format . - Submit a pull request with a clear description of your changes.
Project-Specific Guidelines #
Requesting New Validators
Before requesting a new validator, please consider the following:
- Can it be composed? Many complex validations can be achieved by combining existing validators with
&,|, andnot(). If it can be easily composed, a new validator might not be necessary. - Is it a common use case? We aim to include validators that are widely applicable (e.g.,
isEmail,isUrl). Provide a real-world example of where the validator would be useful. - Propose an API. Suggest a name and signature for the new validator. For example:
isCreditCard(),hasMinLength(5).
Code Style
This project follows the official Dart style guide. All code should be formatted with dart format . before committing.