Common concepts
JSON validation
Emblazon lets you validate JSON responses using templates instead of assertions. Under the hood these templates map back to a zod schema.
Validating JSON responses using assertions can be quite verbose and difficult to maintain. Using a template to validate the response is much more concise and easier to read / visualise the expected structure of the JSON object.
Another motivation is removing the security issues around running user submitted code to validate a JSON object.
You can see different working examples here: Emblazon test suite validation examples.
The simplest example
{
"hello": "world"
}
The simplest template to validate this JSON object would be:
{
"hello": "world"
}
It's not very exciting, but it does validate the JSON object.
Compared to assertions
Let's say we have some JSON that looks like this:
[
{
"id": "cd09281e-af21-4eeb-b43d-49f31b0693af"
}
]
To validate this using assertions, we would need to write something like this (this is copied from Insomnia's website):
const body = JSON.parse(response.data);
const item = body[0];
expect(body).to.be.an('array');
expect(item).to.be.an('object');
expect(item).to.have.property('id');
This is quite verbose and it's hard to see what exactly is being validated.
For example this is the template version, which is more concise and actually validates more (i.e. that the string is a valid UUID):
[
{
"id": "string(uuid)"
}
]
Value validation
Values can either be a specific value like "cd09281e-af21-4eeb-b43d-49f31b0693af" or have a more generic type definition.
At the moment just string and number types are supported.
The general format for a type is type(options)
. Note that the options are optional, meaning we could just write string
. This would check that the value is a string.
Multiple options can be specified and should be separated by a space or comma. E.g:
"string(regex = '.*\\.zip', len > 4)"
Strings
The different options for strings are
- regex - Validates that the string matches the given regex
"string(regex = '.*.zip')"
- len - Validates that the string is exactly the given length
"string(len > 5)"
"string(len >= 5)"
"string(len < 5)"
"string(len <= 5)"
"string(len = 5)"
The following options don't take any parameters
- email - Validates that the string is a valid email address
- url - Validates that the string is a valid URL
- uuid - Validates that the string is a valid UUID
- cuuid - Validates that the string is a valid CUUID
- cuuid2 - Validates that the string is a valid CUUID2
Numbers
Numbers support the different comparison operators: =
, >
, >=
, <
, <=
.
For example:
"number(>5)"
"number(>=5)"
"number(<5)"
"number(<=5)"
"number(=5)"
Reusable types
Sometimes you might want to reuse a type definition.
In objects
For example if you have a JSON object that looks like this:
{
"lastRead": {
"id": "cd09281e-af21-4eeb-b43d-49f31b0693af",
"title": "My book",
"author": "John Doe"
},
"favourite": {
"id": "393775e4-6ac7-41b3-90bd-f48133d6cea2",
"title": "My other book",
"author": "Jane Doe"
}
}
Here the lastRead
and favourite
elements share the same structure. Rather than duplicating the definition of the object we can define a type and then use it in the template:
{
"check:types": {
"Book": {
"id": "string(uuid)",
"title": "string",
"author": "string"
}
},
"lastRead": {
"check:type": "Book"
},
"favourite": {
"check:type": "Book"
}
}
The check:types
object defines the types that can be used in the template. The key is the name of the type and the value is the definition of the type.
The check:type
property is used to specify that the object should be validated using the given type.
In arrays
Say we have the follow
{
"files": [
{
"id": "cd09281e-af21-4eeb-b43d-49f31b0693af",
"name": "file1.zip",
"size": 1234
},
{
"id": "393775e4-6ac7-41b3-90bd-f48133d6cea2",
"name": "file2.zip",
"size": 5678
},
{
"id": "e3b0c442-98fc-11d8-8eb2-f2801f1b9fd1",
"name": "file3.zip",
"size": 9012
}
]
}
If we were to apply the type logic as described above we would end up with:
{
"check:types": {
"FileSummary": {
"id": "string(uuid)",
"name": "string(regex = '.*\\.zip')",
"size": "number(>0)"
}
},
"files": [
{
"check:type": "FileSummary",
},
{
"check:type": "FileSummary",
},
{
"check:type": "FileSummary",
}
]
}
But this is quite verbose, with a lot of duplication - imagine if we had 100 elements in the array!
{
"check:types": {
"FileSummary": {
"id": "string(uuid)",
"name": "string(regex = '.*\\.zip')",
"size": "number(>0)"
}
},
"files": [
{
"check:array": true,
"type": "FileSummary",
"length": 3
}
]
}
The check:array
property specifies that this object describes metadata about the array.
The type
property:
- Can be the name of one of the types defined in the
check:types
object. - Can be the constant
"string"
or"number"
to specify that the array should contain strings or numbers.
The length
property:
- If it is a number then it specifies the exact length of the array
- If it is a string then you can specify a value like
"5+"
to specify that the array must be at least 5 elements long.
The type
and length
properties are both optional. If they are not specified then that validation will not be performed.
Extending types
In the array example above might want to specify a specific name
for the first element in the array.
We can do this by adding an element after the check:array
object. This can be used to add a specific value for a property or to add extra fields to validate. For example:
{
...
"files": [
{
"check:array": true,
"type": "FileSummary",
"length": 3
},
{
"name": "file1.zip",
"another": "property"
}
]
}
We've added extra validation for the first element of the array:
- The
name
property must be exactlyfile1.zip
- The
another
property must be present and have the valueproperty
Trade offs
Using a template to validate a JSON object isn't as flexible as using assertions. Since assertions are defined using javascript code, you can do anything you want, but with templates you're limited to what is supported by the template language.
Having said that we hope that the simplicity and speed of using templates will outweigh the drawbacks in a lot of use cases.
Thanks!
We'd love for you to give it a go and let us know what you think.