Test JSON schema with AJV and Jest
March 20, 2019
While I worked on a project I was involved (Coding-Coach if you must know đ) I faced with an interesting challenge.
The project users âdatabaseâ is actually a big JSON file which contains all the entries. (Why? this is for another post)
This file has some restrictions to make sure the data will be consistency. For instance, âcountryâ field which its value can be only from a list.
Each user can add himself to that JSON file with PR, and travis-ci is running the test which should verify the schema (among other things).
So how can we sure the new member added himself with the right schema without review it in each PR?
AJV is âThe fastest JSON Schema Validatorâ. I didnât check how fast it is but I can tell that its API is nice, flexible, well documented (except 1 case. We will talk about it later) and I liked to work with it.
So, how we do this?
TLDâR include the validation call in a it
spec and fail the test if you get an error from the validation method.
Letâs take their example as a base:
1var Ajv = require('ajv');2var ajv = new Ajv();3var validate = ajv.compile(schema);4var valid = validate(data);5if (!valid) console.log(validate.errors);
Letâs say, our schema should looks like:
1{2 "fullname": "your full name"3}
So the user will have a fullname
field and letâs say has minimum of 2 characters, the validation code will be:
1import users from './users.json';2
3const shema = {4 "type": "array",5 "items": {6 "type": "object",7 "properties": {8 "fullname": {9 "type": "string",10 "minLength": 211 },12 },13 },14};15
16ajv.validate(shema, users);
users.json
1[2 {3 "fullname": "john doe"4 }5]
ajv
object has errors
property â Array with the errors so if the JSON will not meet the schema, the errors
will be:
1[2 {3 keyword: 'minLength',4 dataPath: '[0].fullname',5 schemaPath: '#/items/properties/fullname/minLength',6 params: { limit: 2 },7 message: 'should NOT be shorter than 2 characters'8 }9]
So far so good but it will not break our test.. Where is the test actually?
Itâs not so hard. We just âwrapâ the validation call with it
.
1it.only(`should user's schema be valid`, () => {2 const schema = {3 "type": "array",4 "items": {5 "type": "object",6 "properties": {7 "fullname": {8 "type": "string",9 "minLength": 210 },11 },12 },13 };14
15 const valid = ajv.validate(shema, [16 {17 fullname: 'a'18 }19 ]);20 expect(valid).toBeTruthy();21});
In this case, the test will fail with the message:
1â should user's schema be valid2
3expect(received).toBeTruthy()4
5Received: false
As you can see, I donât know whatâs wrong, I just know that the dataâs schema is invalid, itâs not good enough. It will be better to show more meaningful message.
As we saw earlier, we get the errors from ajv ( ajv.errors
) remember? So we can display it when the test is failing.
In Jest (unlike other libraries), you canât specify the error message. So, in order to display a meaningful error message, we will have to create a custom matcher to show our own message. We also, collect the field name and the index so we could know which one is the problematic and what field.
1expect.extend({2 toBeValid(isValid, errorMessage) {3 return {4 message: () => isValid ? '' : errorMessage,5 pass: isValid6 }7 }8});9
10it(`should user's schema be valid`, () => {11 const shema = {12 "type": "array",13 "items": {14 "type": "object",15 "properties": {16 "fullname": {17 "type": "string",18 "minLength": 219 },20 },21 },22 };23
24 const valid = ajv.validate(shema, [25 {26 fullname: 'a'27 }28 ]);29 try {30 const [, index, fieldName] = /\[(.*)\].(.*)/.exec(error.dataPath);31 return `error with item #${index}'s field "${fieldName}". The error is: ${error.message}`;32 } catch (error) {33 return error.message;34 }35 }).join('\n');36 expect(valid).toBeValid(errorMessage);37});
This one will show the error message we wanted:
1â should user's schema be valid2
3error with item #0's field "fullname". The error is: should NOT be shorter than 2 characters
The next field I wanted to validate is an avatar URL. It shouldnât be a problem because with AJV we get a URL validation out of the box (Hint: by using format: âuri'
).
The problem is, our website is running on https
schema so we wanted that all the resources will come from https
either (to avoid Mixed Content issue). URL with http
is valid so how can we verify the the URL schema?
AJV let us to define âCustom Keywordsâ â a custom function validation of top of the original function based on the type
of the field.
Here is how:
1const validateSecuredUrl = function (schema, uri) {2 return uri.indexOf('https://') === 0;3};4
5ajv.addKeyword('securedUrl', {6 validate: validateSecuredUrl,7 errors: true8});9
10it(`should user's schema be valid`, () => {11 const shema = {12 "type": "array",13 "items": {14 "type": "object",15 "properties": {16 "fullname": {17 "type": "string",18 "minLength": 219 },20 "avatar": {21 "type": "string",22 "format": "uri",23 "securedUrl": true,24 },25 },26 },27 };28
29 const valid = ajv.validate(shema, [30 {31 fullname: 'ab',32 avatar: "http://mywebsite.com/path/to/avatar"33 }34 ]);35 const errorMessage = (ajv.errors || []).map(error => {36 try {37 const [, index, fieldName] = /\[(.*)\].(.*)/.exec(error.dataPath);38 return `error with item #${index}'s field "${fieldName}". The error is: ${error.message}`;39 } catch (error) {40 return error.message;41 }42 }).join('\n');43 expect(valid).toBeValid(errorMessage);44});
And the output will be
1â should user's schema be valid2
3error with item #0's field "avatar". The error is: should pass "securedUrl" keyword validation
The only thing that left is to display whatâs exactly secureUrl
validation is.
The docs does mention how to return a custom error message
The function should return validation result as boolean. It can return an array of validation errors via
.errors
property of itself (otherwise a standard error will be used).
But I wasnât sure how exactly to implement it (whoever thinks that it straightforward, well done đ)
âŠwell documented (except 1 case. We will talk about it later)
Remember?
So after diving into the repo in Github, I found the answer in an issue who asks exactly that.
So the full code will be
1const validateSecuredUrl = function (schema, uri) {2 validateSecuredUrl.errors = [{keyword: 'secured', message: 'avatar url must be "https" schema', params: {keyword: 'secured'}}];3 return uri.indexOf('https://') === 0;4};5
6ajv.addKeyword('securedUrl', {7 validate: validateSecuredUrl,8 errors: true9});10
11it(`should user's schema be valid`, () => {12 const shema = {13 "type": "array",14 "items": {15 "type": "object",16 "properties": {17 "fullname": {18 "type": "string",19 "minLength": 220 },21 "avatar": {22 "type": "string",23 "format": "uri",24 "securedUrl": true,25 },26 },27 },28 };29
30 const valid = ajv.validate(shema, [31 {32 fullname: 'ab',33 avatar: "http://mywebsite.com/path/to/avatar"34 }35 ]);36 const errorMessage = (ajv.errors || []).map(error => {37 try {38 const [, index, fieldName] = /\[(.*)\].(.*)/.exec(error.dataPath);39 return `error with item #${index}'s field "${fieldName}". The error is: ${error.message}`;40 } catch (error) {41 return error.message;42 }43 }).join('\n');44 expect(valid).toBeValid(errorMessage);45});
And the output will be
1â should user's schema be valid2
3error with item #0's field "avatar". The error is: avatar url must be "https" schema
And now the user could understand exactly what he / she needs to fix.
Original Post