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.
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.
dart pub add eskema
Validate a map using a schema-like validator and get back a detailed result.
import 'package:eskema/eskema.dart';
final userValidator = eskema({
// Use built-in validator functions
'username': all([isString(), isNotEmpty()]),
// Some zero-arg validators also have cached aliases (e.g. $isBool, $isString), more efficient
'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(isDate()),
});
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
}
- Eskema
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.
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.
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
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.
- 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.
- Use
validateAsync()
+ checkr.isValid
. - Or, wrap an async chain with
throwInstead(v)
and handleValidatorFailedException
inside atry/catch
. - Misuse (calling
.validate()
on async chain) →AsyncValidatorException
.
Most existing synchronous validators require no changes. Only update call sites to .validateAsync()
where you introduce an async validator.
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)
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',
});
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)]);
});
}
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
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.
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.
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.
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.
- 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.
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)
.
This project follows the official Dart style guide. All code should be formatted with dart format .
before committing.