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)
→ throwsValidatorFailedException
on 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 throwsAsyncValidatorException
with 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
Future
interface 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 handleValidatorFailedException
inside 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
(unlessnullable
is 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.
Libraries
- 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.
- expectation
- extensions
- result
- Represents the result of a validation.
- transformers
- Transformers
- validator
- Validator library
- validators
- Top‑level built‑in validators and combinators.
- validators/cached
- Cached Validators
- validators/combinator
- Combinator Validators
- validators/comparison
- Comparison Validators
- validators/list
- List Validators
- validators/number
- Number Validators
- validators/presence
- Presence Validators
- validators/string
- String Validators
- validators/structure
- Structure Validators
- validators/type
- Type Validators