Hey there 👋
Hope you're having a great day! I think I am, as today is the day I announce a project I've been working on for the past weeks.
I'm happy to introduce you to Eskema, a tool to help you validate dynamic data in Dart & Flutter.
The issue is I'm not certain if Eskema is useful or not, so here is where I need your help. Let me know if you think this package could be useful and for what purpose.
Before continuing let me show you a little snippet showcasing what Eskema looks like (I further explain this later):
final validateMap = eskema({
'name': isType<String>(),
'address': nullable(
eskema({
'city': isType<String>(),
'street': isType<String>(),
'number': all([
isType<int>(),
isMin(0),
]),
'additional': nullable(
eskema({
'doorbel_number': isType<int>(),
})
),
})
)
});
Description
Eskema is a set of tools to help you validate dynamic data. It allows you to define data "schemes" and "validators" to validate a value, a Map or List for which we don't have the control to correctly type. You can think of it as a really lightweight json schema type thing.
For example, let's say you want to build a library, app or tool where you expect the user to pass in some data but don't want to, or can't use classes or known data structures. This would be the ideal scenario to use Eskema.
Motivation
The main motivation to build this package was that I did not find a solution to this problem that I liked, and decided to build it myself. I did not have a real use case for this at the moment of building it, but I think there are some scenarios where Eskema can be very useful.
Some solutions required to generate code from annotations before being able to validate data, for example using json_serializable.
If you're already using the json_serializable package, you can make use of it to validate you data.
I wanted to try and build a tool that didn't require to generate any code, it's all handled at runtime.
Design
I made sure from the start that the package was flexible, expansible and composable. I also made sure to properly document almost the whole package. I wanted the package to be as well tested as possible, at the moment it has a 100% coverage and is almost fully tested.
The package should offer the basic API and tools to be able to validate any type of data (e.g. Map, List, bool, int, etc...)
I also made sure it would be very simple to create new types of validators as to not limit you!
How does it work?
The package is really simple, everything in the package are Validator
s, which are just functions that receive a value and return a IResult
.
Parting from this premise, the package adds some helpful Validators
to validate single fields, map fields (eskema) and list fields (listEskema, listEach). They all behave in the same way, making composability really straightforwards and powerful.
final validateMap = eskema({
'name': isType<String>(),
'address': nullable(
eskema({
'city': isType<String>(),
'street': isType<String>(),
'number': all([
isType<int>(),
isMin(0),
]),
'additional': nullable(
eskema({
'doorbel_number': isType<int>(),
})
),
})
)
});
The above example, validates a map against the eskema defined.
Usage
NOTE: that if you only want to validate a single value, you probably don't need Eskema.
Otherwise let's check how to validate a single value. You can use validators individually:
final isString = isType<String>();
const result1 = isString('valid string');
const result2 = isString(123);
result1.isValid; // true
result2.isValid; // false
result2.expected; // String
Or you can combine validators:
all([isType<String>(), isDate()]); // all validators must be valid
or(isType<String>(), isType<int>()); // either validator must be valid
and(isType<String>(), isType<int>()); // both validator must be valid
// This validator checks that, the value is a list of strings, with length 2, and contains item "test"
all([
isOfLength(2), // checks that the list is of length 2
listEach(isTypeOrNull<String>()), // checks that each item is either string or null
listContains('test'), // list must contain value "test"
]);
// This validator checks a map against a eskema. Map must contain property 'books',
// which is a list of maps that matches a sub-eskema.
final matchesEskema = eskema({
'books': listEach(
eskema({
'name': isType<String>(),
}),
),
});
matchesEskema({'books': [{'name': 'book name'}]});
Validators
isType
This validator checks that a value is of a certain type
isType<String>();
isType<int>();
isType<double>();
isType<List>();
isType<Map>();
isTypeOrNull
This validator checks that a value is of a certain type or is null
isTypeOrNull<String>();
isTypeOrNull<int>();
nullable
This validator allows to make validators allow null values
nullable(eskema({...}));
- The validator above, allows a map or null
eskema
The most common use case will probably be validating JSON or dynamic maps. For this, you can use the eskema
validator.
In this example we validate a Map with optional fields and with nested fields.
final validateMap = eskema({
'name': isType<String>(),
'address': nullable(
eskema({
'city': isType<String>(),
'street': isType<String>(),
'number': all([
isType<int>(),
isMin(0),
]),
'additional': nullable(
eskema({
'doorbel_number': Field([isType<int>()])
})
),
})
)
});
final invalidResult = validateMap({});
invalidResult.isValid; // false
invalidResult.isNotValid; // true
invalidResult.expected; // name -> String
invalidResult.message; // Expected name -> String
final validResult = validateMap({ 'name': 'bobby' });
validResult.isValid; // true
validResult.isNotValid; // false
validResult.expected; // Valid
listEach
The other common use case is validating dynamic Lists. For this, you can use the listEach
class.
This example validates that the provided value is a List of length 2, and each item must be of type int:
final isValidList = all([
listOfLength(2),
listEach(isType<int>()),
]);
isValidList(null).isValid; // true
isValidList([]).isValid; // true
isValidList([1, 2]).isValid; // true
isValidList([1, "2"]).isValid; // false
isValidList([1, "2"]).expected; // [1] -> int
Additional Validators
For a complete list of validators, check the docs
Custom Validators
Eskema offers a set of common Validators located in lib/src/validators.dart
. You are not limited to only using these validators, custom ones can be created very easily.
Let's see how to create a validator to check if a string matches a pattern:
Validator validateRegexp(RegExp regexp) {
return (value) {
return Result(
isValid: regexp.hasMatch(value),
expected: 'match pattern $regexp', // the message explaining what this validator expected
);
};
}
Summary
Just to finish up, I want to thank you for taking the time to read though all of that. If you liked the project please feel free to share and/or star it on GitHub, so it can reach more people who might find it useful.
Remember to let me know if you think this package is useful to you and why and where would you use it!
Help is encouraged!
If you feel like helping out, have any ideas or just want to support this project please feel free to do so!!