Every Dart project has that one file — you know the one.
The file full of if (value == null || value is! String || value.isEmpty)
checks.
The file that starts out as “quick validation” and slowly mutates into a plate of spaghetti code that nobody dares to touch. 🍝
I’ve been there. In fact, that’s what pushed me to build Eskema.
Most existing solutions I found leaned on code generation, which meant I ended up with opinionated, hard-to-read boilerplate and mysterious generated classes. I wanted something else: a library that was declarative, ergonomic, and unopinionated. Something that makes it obvious what is being validated and where it’s happening, without sprinkling spaghetti across my codebase.
Eskema started as a tiny functional library a few years ago, but it has since grown up: now it has a solid class-based core, operator sugar, and a builder API. It’s still extremely simple to extend; adding a new validator or transformer is basically trivial, but it’s powerful enough to cover most real-world validation needs.
Why Eskema?
Key features of Eskema include:
Composable API: Validators are just Dart functions that take a value and return a
Result
. You can nest and combine them freely (withall()
,or()
, or the overloaded&
/|
operators). This makes validation logic highly composable and easy to reason about.Rich built-in validators: Out of the box, Eskema has checks for types, numbers, strings, lists, maps, etc. For example,
isString()
,isInt()
,isEmail()
,listEach()
,listIsOfLength()
, and many more. Presence checks (isNotNull()
,isNotEmpty()
,isPresent()
) and comparison checks (isGt()
,isLte()
,isIn()
, etc.) are included too.Operator sugar: Combine validators with
&
(AND) and|
(OR), and invert withnot()
. For example, instead ofall([isString(), isNotEmpty()])
, you can simply write$isString & isNotEmpty()
. You can also override error messages inline with the>
operator, e.g.hasLength(5) > 'must be 5 chars'
.Optional vs Nullable semantics: By default, a key must be present and non-null. Use
nullable(validator)
to allow a field to benull
, oroptional(validator)
to allow it to be missing entirely. This helps avoid the common confusion between “no value” and “null value.”Builder API: If you prefer a more fluent, type-safe style, Eskema offers a builder API. For example:
final userValidator = $map().schema({
'id': $string().trim().toIntStrict().gt(0),
'email': $string().trim().toLowerCase().email(),
'age': $string().toIntStrict().gte(18).optional(),
});
No codegen – all runtime: Eskema validates plain Dart maps and values at runtime. No build steps, no generated classes.
Production-ready: Fully tested, documented, and with sensible error messages. Failures produce a structured list of expectations (message, field path, etc.), not just a bool.
Functional Validation Examples
Here’s a validator for a user object using the functional API:
final userSchema = eskema({
'username': isString() & isNotEmpty(),
'password': isString() & hasLength(8, 32),
'email': isString() & isEmail(),
'signupDate': optional(isDateTime()),
});
final result = userSchema.validate({
'username': 'alice',
'password': 'secret123',
'email': 'alice@example.com',
// signupDate omitted is OK (optional)
});
if (!result.isValid) {
print(result.expectations);
}
Each field maps to a validator. We combined checks with & instead of writing all([...])
, and used optional(isDateTime())
to allow signupDate to be missing. The Result object has .isValid
and .expectations
with detailed error info.
If you only need to validate a single value:
final isNonEmptyStr = isString() & isNotEmpty();
print(isNonEmptyStr.isValid('hello')); // true
print(isNonEmptyStr.validate('').isValid); // false
Writing custom validators is easy too:
Validator isEven = Validator((value) {
return Result(
isValid: value is int && value % 2 == 0,
expected: 'even integer',
);
});
Before vs After Eskema
Let’s be honest: we’ve all written validation code like this at some point:
// Before: classic if/else soup
bool validateUser(Map<String, dynamic> user) {
if (user['username'] == null || user['username'] is! String || user['username'].isEmpty) {
return false;
}
if (user['password'] == null || user['password'] is! String) {
return false;
}
if ((user['password'] as String).length < 8 || (user['password'] as String).length > 32) {
return false;
}
if (user['email'] == null || user['email'] is! String) {
return false;
}
final email = user['email'] as String;
final emailRegex = RegExp(r'^[^@]+@[^@]+\.[^@]+');
if (!emailRegex.hasMatch(email)) {
return false;
}
return true;
}
Not only is this verbose, but it’s brittle: hard to extend, hard to read, and easy to get wrong.
Now here’s the same logic expressed with Eskema:
// After: Eskema
final userSchema = eskema({
'username': isString() & isNotEmpty(),
'password': isString() & hasLength(8, 32),
'email': isString() & isEmail(),
});
final result = userSchema.validate({
'username': 'alice',
'password': 'secret123',
'email': 'alice@example.com',
});
print(result.isValid); // true
Clean, declarative, and composable. Instead of juggling if/else and regex checks, you just describe what you expect — and Eskema takes care of the rest.
Using the Builder
For those who like a fluent, IDE-friendly approach, Eskema’s builder API provides similar power with method chaining:
NOTE that everything is a validator, so you can combine and compose functional and builder validators.
final mapValidator = $map().schema({
'id': $string().trim().toIntStrict().gt(0),
'tags': $list().each($string()).lengthMin(1),
});
// Usage:
final res = mapValidator.validate({'id': '42', 'tags': ['dart', 'eskema']});
print(res.isValid); // true
This yields the same result as manual validators, but in a fluent syntax. The advantage lies in ergonomics: IDE autocompletion, type safety, and the elimination of the need to import numerous free functions.
Ergonomics and Tips
Use the
$
shortcuts: Many zero-arg validators have a cached alias. For example,$isString
is a pre-builtisString()
. Looks clean and avoids re-allocating functions. UseisString()
if you want to specify a custom error message.Operator overloading: Get comfortable with
&
,|
andnot()
. They make schemas terser and more logical to read.Clear error messages: Use
>
to override messages, e.g.hasLength(5) > Expectation(...)
.No boilerplate: Works out of the box for Flutter forms, API handlers, config files – anywhere you need validation.
Async support: Need async checks? Just use
.validateAsync()
. Eskema will promote validators to async as needed.
Conclusion
Eskema takes the headache out of runtime validation by providing a simple and flexible toolkit. It’s small in scope but big in composability. You can validate nested JSON, lists, or individual values with clear syntax and rich error reporting – all without codegen or ceremony.
If your app deals with dynamic data, give Eskema a try. A few ergonomic validators might just save you from a world of edge-case bugs – and maybe even a little coding despair.
👉 Give it a spin: Eskema on GitHub. Your future self (and your teammates) will thank you.”
Top comments (0)