Hello! Let's talk a little about data validation. What is complicated and why should it be needed, say, in a project written in typescript? Typescript controls pretty well everything, it remains to check the user input as much as possible. That is, to throw a dozen regulars into the project and it would seem that you can close the topic, but ... Far from always, and in the case of the web, almost never, the whole project is in a single code base and uses the same types. At the junction of such code bases, situations arise when the wait does not correspond to reality and here typescript is no longer an assistant. A few examples:
- The client part of the application receives data from the API and validates it. Firstly, the API can suddenly and sometimes without notice change, and secondly, the "server guys" themselves sometimes do not know what their API can do, for example, in some field, instead of a guaranteed array, even if empty, the full moon may be given
null
. When describing data on the client, programmers determine what the client knows how to work with and if something goes wrong, then it is much more pleasant to immediately see a message in the console about the source of the problem, rather than picking up an incomprehensible bug already where it got out in the view layer (and it’s good if it is immediately noticed). Also now there are already solutions ( 1 , 2 ) that allow transferring types from server to client. I have not tried to do so yet, but it is quite possible that this is the future. - The reverse situation is when the server checks the sent parameters to immediately stop processing the request if they do not meet the expected ones. I think there is no need for any details on why this is important.
- Validation of data before storing it in the database will also not be superfluous. For example, you can see how it is organized in one of my bikes: MaraquiaORM # Validation .
I think the examples are quite convincing and now there is no feeling that you can do with simple regulars, because it's not just about user input, but about the validation of complex, usually nested on several levels of data. A special library is already needed here. And of course there are! It just so happens that over the past 10 years, each time starting a new project, I try to use another such library in it, adjusting it to my needs. And every time something goes wrong, which sometimes leads to the replacement of the test subject in the middle of active development. I will not talk about all the options I studied, I’ll only say about those tested in the current project.
type-check
Small and quite convenient library. The circuit is described as a string. Using multi-line strings, you can describe fairly complex structures:
`{ ID: String, creator: { fname: String | Null, mname: String | Null, lname: String | Null, email: [String] } | Undefined, sender: Maybe { name: String, email: String }, type: Number, subject: String, ... }`
There are quite serious drawbacks:
- The IDE does not help with a set of schemas, which became especially annoying when switching to typescript.
- Virtually useless error messages. I have not used this library for more than a year and maybe something has changed (judging by the code, no). The messages were in the style "Expected string received null". Now imagine, you have an array of pieces for 200 objects, each of which has fields with strings, and only one object has one of the fields broken. How to find this field? View all 200 items? I suffered so many times and it really
broke my life,ruined the impression of the library. Usually I don’t want to know what was expected and received there, but I want to open the data schema and find the field needed in it and the same in the data itself. In other words, in the error message, it is critical to have the keypath to the right place in the data / schema, and what was expected there and arrived at all can be omitted. - Quite a trifle, of course, but the indentation in the example above will not disappear when the code is compressed.
Joi
Github
Browser Version: joi-browser
Probably the most famous library on this subject with a bunch of features and an endless API. At first I used it on the server and it showed itself perfectly. At some point, I decided to replace it with type-check
on the client. At that time, I almost did not control the size of the bundle, there were simply no problems with this. But over the year it has grown rapidly and on the mobile Internet the first application download became completely uncomfortable. It was decided to organize a lazy component loading. The webpack-bundle-analyzer report showed a bunch of giants in the bundle and they all went easily to the chunks created by the webpack. Everyone except Joi
. Many components communicate with the server and all server responses are validated, that is, putting Joi
into some kind of chunk does not make sense, it will simply always load right after the main one. At some point, the main bundle looked like this: tyts . Of course, a lasting desire arose to do something about it. I wanted the same convenient library, but much less.
Yup
In the readme they promise about the same Joi
, but in size suitable for the frontend. In fact, it is only about two times smaller, that is, Yup
was still the largest library in the main bundle. In addition, additional disadvantages appeared:
- The library skips all
undefined
by default. Constantly writing.required()
not very pleasant, and I like it better when initially everything is impossible and where it is allowed.Joi
has an option forpresence: 'required'
to configure this behavior. I created a request with the hellish number 666 , but so far the authors are silent. - There is no way to check the values of an object, allowing all keys.
Joi
uses object.pattern for this, with the first argument accepting any string. Probably here it would still be possible to somehow get out, and the authors can correct the first minus, but given the size, I did not want to wait or edit something myself.
Ow
The next applicant finally turned out to be really small, plus he didn’t make him constantly write ()
where you can do without it. For example, you can write a validator that allows a string or undefined
as follows:
let optionalStringValidator = ow.optional.string; ow(optionalStringValidator, '1'); // Ok ow(optionalStringValidator, undefined); // Ok
Great! What about null
? Turning all the documentation over, I found the following method:
ow.any(ow.optional.string, ow.null);
Oh God! When I tried to rewrite part of the validation in the project, I almost broke my fingers while typing this. I ow.nullable
issue on adding ow.nullable
, which was sent here . In short, they say that null
is not needed at all. The arguments given there are also quite adequate given the first line in their readme:
Function argument validation for humans
That is, this library is for validating the values that come as arguments to the function. Apparently, they didn’t really count on huge nested structures coming from the server.
Further study and attempts to use revealed several more features that, again, were well explained by the same line in the readme, but did not suit me very well. This is actually a pretty good library, it is just for a few other purposes.
Around here, I was completely tired of giving up and decided to write my own library with blackjack and virgins. Yes, yes, I'm back to you with the next bike :). Meet:
Omyumyum
A few examples:
import om from 'omyumyum'; const isOptionalNumber = om.number.or.undefined; isOptionalNumber('1'); // => false isOptionalNumber(1); // => true isOptionalNumber(undefined); // => true
.or
can be used as many times as you want by increasing the feasible options:
om.number.or.string.or.null.or.undefined;
In this case, an almost ordinary function is constantly generated that takes any argument and returns a boolean
.
If you want the function to throw an error if the test fails:
om(om.number, '1'); // TypeError
Or with currying:
const isNumberOrThrow = om(om.number); isNumberOrThrow('1') // TypeError
The resulting function is not quite ordinary, as it has additional methods. .or
already shown, part of the methods will depend on the selected type (see API ), for example, a string can be enhanced with a regular expression:
const isNonEmptyString = om.string.pattern(/\S/); // == `om.string.nonEmpty` isNonEmptyString(' '); // => false isNonEmptyString('1'); // => true
And for the object, you can specify its shape:
const isUserData = om.object.shape({ name: om.string, age: om.number.or.vacuum // `.or.vacuum` == `.or.null.or.undefined` }); isUserData({}); // => false isUserData({ age: 20 }) // => false isUserData({ name: '' }); // => true isUserData({ name: '', age: null }); // => true isUserData({ name: '', age: 20 }); // => true
The promised keypath to the problem spot:
om(om.array.of(om.object.shape({ name: om.string })), [{ name: '' }, { name: null }]); // TypeError('Type mismatch at "[1].name"')
If the built-in features are not enough, you can always use .custom(validator: (value: any) => boolean)
:
const isEmailOrPhone = om.custom(require('is-email')).or.custom(require('is-phone')); isEmailOrPhone('test@test.test'); // => true
In stock is also the expected .and
used to combine and improve types:
const isNonZeroString = om.string.and.custom(str => str.length > 0); // == `om.string.nonZero` isNonZeroString(''); // => false isNonZeroString('1'); // => true
.and
takes precedence over .or
, but since .custom()
accepts the validator of exactly the same form as created by the library, this can be circumvented:
// `age`, `birthday` om.object.shape({ name: om.string }).and.custom( om.object.shape({ age: om.number }) .or.object.shape({ birthday: om.date })] );
You can continue to improve previously created validators. The old ones do not spoil at all. Let's try to improve the isUserData
created earlier:
const isImprovedUserData = isUserData.and.object.shape({ friends: om.array.of(isUserData).or.vacuum }); isImprovedUserData({ name: '', age: 20, friends: [{ name: '', age: 18 }] }); // => true
Well, stayed .not
:
const isNotVacuum = om.not.null.and.not.undefined; // == `om.not.vacuum` isNotVacuum(1); // => true isNotVacuum(null); // => false isNotVacuum(undefined); // => false
Other available methods can be found in the library API .
Advantages of the library:
- Laconic syntax with
.or
,.and
,.not
and a minimum of brackets. In combination with autocomplete typescript, the set turns into a pleasure. - Tiny weight even compared to
Ow
(almost 10 times less (minify + gzip)), and compared toJoi
library is like a feather next to a mountain. - Nice name :)
Cons of the library:
- Fewer types and their modifiers. And it is unlikely that there will be much more. All three usage scenarios given at the beginning of the article (something about junctions and code bases) assume the transmission of plain text data, in most cases this is JSON. That is, in my opinion, such a library should support the types possible in JSON, plus
undefined
and a few commonly used types. The sameOw
for some reason is crammed with support for all types of typed arrays and other nonsense. I think this is superfluous. - Does not know how to convert data like
Joi
. I thinkJoi
also pretty bad at it. At least I don’t have enough of its capabilities and, if necessary, I make transformations with completely different tools. Perhaps this is a further development direction foromyumyum
.
Everything! If you liked the article, like it, subscribe to the channel and good luck)).