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 exactly file1.zip
  • The another property must be present and have the value property

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.

Previous
Getting started