Advertisement
  1. Code
  2. Coding Fundamentals

Validating Data With JSON-Schema: Part 1

Scroll to top
This post is part of a series called Validating Data With JSON-Schema.
Validating Data With JSON-Schema, Part 2

When you're dealing with complex and structured data, you need to determine whether the data is valid or not. JSON-Schema is the standard of JSON documents that describes the structure and the requirements of your JSON data. In this two-part series, you'll learn how to use JSON-Schema to validate data.

Let's say you have a database of users where each record looks similar to this example:

1
{
2
  "id": 64209690,
3
  "name": "Jane Smith",
4
  "email": "jane.smith@gmail.com",
5
  "phone": "07777 888 999",
6
  "address": {
7
    "street": "Flat 1, 188 High Street Kensington",
8
    "postcode": "W8 5AA",
9
    "city": "London",
10
    "country": "United Kingdom"
11
  },
12
  "personal": {
13
    "DOB": "1982-08-16",
14
    "age": 33,
15
    "gender": "female"
16
  },
17
  "connections": [
18
    {
19
      "id": "35434004285760",
20
      "name": "John Doe",
21
      "connType": "friend",
22
      "since": "2014-03-25"
23
    },
24
    {
25
      "id": 13418315,
26
      "name": "James Smith",
27
      "connType": "relative",
28
      "relation": "husband",
29
      "since": "2012-07-03"
30
    }
31
  ],
32
  "feeds": {
33
    "news": true,
34
    "sport": true,
35
    "fashion": false
36
  },
37
  "createdAt": "2022-08-31T18:13:48.616Z"
38
}

The question we are going to deal with is how to determine whether the record like the one above is valid or not.

Examples are very useful but not sufficient when describing your data requirements. JSON-Schema comes to the rescue. This is one of the possible schemas describing a user record:

1
{
2
  "$schema": "https://json-schema.org/draft-04/schema#",
3
  "id": "https://mynet.com/schemas/user.json#",
4
  "title": "User",
5
  "description": "User profile with connections",
6
  "type": "object",
7
  "properties": {
8
    "id": {
9
      "description": "positive integer or string of digits",
10
      "type": ["string", "integer"],
11
      "pattern": "^[1-9][0-9]*$",
12
      "minimum": 1
13
    },
14
    "name": { "type": "string", "maxLength": 128 },
15
    "email": { "type": "string", "format": "email" },
16
    "phone": { "type": "string", "pattern": "^[0-9()\-\.\s]+$" }, 
17
    "address": {
18
      "type": "object",
19
      "additionalProperties": { "type": "string" },
20
      "maxProperties": 6,
21
      "required": ["street", "postcode", "city", "country"]
22
    },
23
    "personal": {
24
      "type": "object",
25
      "properties": {
26
        "DOB": { "type": "string", "format": "date" },
27
        "age": { "type": "integer", "minimum": 13 },
28
        "gender": { "enum": ["female", "male"] }
29
      }
30
      "required": ["DOB", "age"],
31
      "additionalProperties": false
32
    },
33
    "connections": {
34
      "type": "array",
35
      "maxItems": 150,
36
      "items": {
37
        "title": "Connection",
38
        "description": "User connection schema",
39
        "type": "object",
40
        "properties": {
41
          "id": {
42
            "type": ["string", "integer"],
43
            "pattern": "^[1-9][0-9]*$",
44
            "minimum": 1
45
          },
46
          "name": { "type": "string", "maxLength": 128 },
47
          "since": { "type": "string", "format": "date" },
48
          "connType": { "type": "string" },
49
          "relation": {},
50
          "close": {}
51
        },
52
        "oneOf": [
53
          {
54
            "properties": {
55
              "connType": { "enum": ["relative"] },
56
              "relation": { "type": "string" }
57
            },
58
            "dependencies": {
59
              "relation": ["close"]
60
            }
61
          },
62
          {
63
            "properties": {
64
              "connType": { "enum": ["friend", "colleague", "other"] },
65
              "relation": { "not": {} },
66
              "close": { "not": {} }
67
            }
68
          }
69
        ],
70
        "required": ["id", "name", "since", "connType"],
71
        "additionalProperties": false
72
      }
73
    },
74
    "feeds": {
75
      "title": "feeds",
76
      "description": "Feeds user subscribes to",
77
      "type": "object",
78
      "patternProperties": {
79
        "^[A-Za-z]+$": { "type": "boolean" }
80
      },
81
      "additionalProperties": false
82
    },
83
    "createdAt": { "type": "string", "format": "date-time" }
84
  }
85
}

Have a look at the schema above and the user record it describes (that is valid according to this schema). There is a lot of explaining to do here.

JavaScript code to validate the user record against the schema could be:

1
const Ajv = require('ajv');
2
const ajv = Ajv({allErrors: true});
3
const valid = ajv.validate(userSchema, userData);
4
if (valid) {
5
  console.log('User data is valid');
6
} else {
7
  console.log('User data is INVALID!');
8
  console.log(ajv.errors);
9
}

or for better performance:

1
const validate = ajv.compile(userSchema);
2
const valid = validate(userData);
3
if (!valid) console.log(validate.errors);

If you are using ESM, do this:

1
import ajv from "ajv"
2
const ajv = Ajv({allErrors: true, code: {esm: true}});
3
const valid = ajv.validate(userSchema, userData);
4
if (valid) {
5
  console.log('User data is valid');
6
} else {
7
  console.log('User data is INVALID!');
8
  console.log(ajv.errors);
9
}

ESM?

ESM (ECMAScript modules) is a module format that is slowly replacing CommonJS (the format that uses require() due to it working in web browsers as well as Node.js and because of the new features it offers.

If you want the most modern code, try using ESM. Otherwise, if you want to stick with the most common current format, just use CJS. In this tutorial, the examples will use CJS, but I recommend trying out ESM as a replacement.

For more information on ESM and how to use it in Node.js, check out this article on ESM.

All the code samples are available in the GitHub repo tutsplus-json-schema. You can also try it in the browser.

Ajv, the validator used in the example, is the fastest JSON-Schema validator for JavaScript. I created it, so I am going to use it in this tutorial.

Before we continue, let's quickly deal with all the whys.

Why Validate Data as a Separate Step?

  • to fail fast
  • to avoid data corruption
  • to simplify processing code
  • to use validation code in tests

Why JSON (and not XML)?

  • as wide adoption as XML
  • easier to process and more concise than XML
  • dominates web development because of JavaScript

Why Use Schemas?

  • declarative
  • easier to maintain
  • can be understood by non-coders
  • no need to write code, third party open-source libraries can be used

Why JSON-Schema?

  • the widest adoption among all standards for JSON validation
  • very mature (current version is 4, there are proposals for version 5)
  • covers a big part of validation scenarios
  • uses easy-to-parse JSON documents for schemas
  • platform independent
  • easily extensible
  • 30+ validators for different languages, including 10+ for JavaScript, so no need to code it yourself

Tasks

This tutorial includes several relatively simple tasks to help you better understand the JSON schema and how it can be used. There are simple JavaScript scripts to check that you've done them correctly. To run them you will need to install node.js (you need no experience with it). Just install nvm (node version manager) and a recent node.js version:

1
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
2
nvm install node

You also need to clone the repo and run npm install (it will install Ajv validator).

Let's Dive Into the Schemas!

JSON-schema is always an object. Its properties are called "keywords". Some of them describe the rules for the data (e.g., "type" and "properties"), and some describe the schema itself ("$schema", "id", "title", "description")—we will get to them later.

The data is valid according to the schema if it is valid according to all keywords in this schema—that's really simple.

Data Properties

Because most JSON data consists of objects with multiple properties, the keyword "properties" is probably the most commonly used keyword. It only applies to objects (see the next section about what "apply" means).

You might have noticed in the example above that each property inside the "properties" keyword describes the corresponding property in your data.

The value of each property is itself a JSON-schema—JSON-schema is a recursive standard. Each property in the data should be valid according to the corresponding schema in the "properties" keyword.

The important thing here is that the "properties" keyword doesn't make any property required; it only defines schemas for the properties that are present in the data.

For example, if our schema is:

1
{
2
  "properties": {
3
    "foo": { "type": "string" }
4
  }
5
}

then objects with or without property "foo" can be valid according to this schema:

1
{foo: "bar"}, {foo: "bar", baz: 1}, {baz: 1}, {} // all valid

and only objects that have property foo that is not a string are invalid:

1
{ foo: 1 } // invalid

Try this example in the browser.

Data Type

You've already figured out what the keyword "type" does. It is probably the most important keyword. Its value (a string or array of strings) defines what type (or types) the data must be to be valid.

As you can see in the example above, the user data must be an object.

Most keywords apply to certain data types—for example, the keyword "properties" only applies to objects, and the keyword "pattern" only applies to strings.

What does "apply" mean? Let's say we have a really simple schema:

1
{ "pattern": "^[0-9]+$" }

You may expect that to be valid according to such schema, the data must be a string matching the pattern:

1
"12345"

But the JSON-schema standard specifies that if a keyword doesn't apply to the data type, then the data is valid according to this keyword. That means that any data that is not of type "string" is valid according to the schema above—numbers, arrays, objects, boolean, and even null. If you want only strings matching the pattern to be valid, your schema should be:

1
{
2
  "type": "string",
3
  "pattern": "^[0-9]+$"
4
}

Because of this, you can make very flexible schemas that will validate multiple data types.

Look at the property "id" in the user example. It should be valid according to this schema:

1
{
2
  "type": ["string", "integer"],
3
  "pattern": "^[1-9][0-9]*$",
4
  "minimum": 1
5
}

This schema requires that the data to be valid should be either a "string" or an "integer". There is also the keyword "pattern" that applies only to strings; it requires that the string should consist of digits only and not start from 0. There is the keyword "minimum" that applies only to numbers; it requires that the number should be not less than 1.

Another, more verbose, way to express the same requirement is:

1
{
2
  "anyOf": [
3
    { "type": "string", "pattern": "^[1-9][0-9]*$" },
4
    { "type": "integer", "minimum": 1 },
5
  ]
6
}

But because of the way JSON-schema is defined, this schema is equivalent to the first one, which is shorter and faster to validate in most validators.

Data types you can use in schemas are "object", "array", "number", "integer", "string", "boolean", and "null". Note that "number" includes "integer"—all integers are numbers too.

Numbers Validation

There are several keywords to validate numbers. All the keywords in this section apply to numbers only (including integers).

"minimum" and "maximum" are self-explanatory. In addition to them, there are the keywords "exclusiveMinimum" and "exclusiveMaximum". In our user example, the user age is required to be an integer that is 13 or bigger. If the schema for the user age were:

1
{
2
  "type": "integer",
3
  "minimum": 13,
4
  "exclusiveMinimum": true
5
}

then this schema would have required that user age is strictly bigger than 13, i.e. the lowest allowed age would be 14.

Another keyword to validate numbers is "multipleOf". Its name also explains what it does, and you can check out the JSON-schema keywords reference to see how it works.

Strings Validation

There are also several keywords to validate strings. All the keywords in this section apply to strings only.

"maxLength" and "minLength" require that the string is not longer or not shorter than the given number. The JSON-schema standard requires that a unicode pair, e.g. emoji character, is counted as a single character. JavaScript counts it as two characters when you access the .length property of strings.

Some validators determine string lengths as required by the standard, and some do it the JavaScript way, which is faster. Ajv allows you to specify how to determine string lengths, and the default is to comply with the standard.

1
var schema = { "maxLength": 2 };
2
3
var ajv = Ajv(); // count unicode pairs as one character
4
ajv.validate(schema, "😀😀"); // true
5
ajv.validate(schema, "😀😀!"); // false
6
7
var ajv = Ajv({unicode: false}); // count unicode pairs as two characters
8
ajv.validate(schema, "😀"); // true
9
ajv.validate(schema, "😀!"); // false

You have already seen the "pattern" keyword in action—it simply requires that the data string matches the regular expression defined according to the same standard that is used in JavaScript. See the example below for schemas and matching regular expressions:

1
{"pattern": "^[1-9][0-9]*$" };   /^[1-9][0-9]*$/;  // id
2
{"pattern": "^[A-Za-z]+$" };     /^[a-z]+$/i;      // letters
3
{"pattern": "^[0-9()\-\.\s]+$"}; /^[0-9()\-\.\s]+$/; // phone

The "format" keyword defines the semantic validation of strings, such as "email", "date" or "date-time" in the user example. JSON-Schema also defines the formats "uri", "hostname", "ipv4", and "ipv6". Validators define formats differently, optimizing for validation speed or for correctness. Ajv gives you a choice:

1
var ajv = Ajv(); // "fast" format validation
2
ajv.validate({"format": "date"}, "2015-12-24"); // true
3
ajv.validate({"format": "date"}, "2015-14-33"); // true
4
5
var ajv = Ajv({format: 'full'); // more thorough format validation
6
ajv.validate({"format": "date"}, "2015-12-24"); // true
7
ajv.validate({"format": "date"}, "2015-14-33"); // false

Most validators allow you to define custom formats either as regular expressions or validating functions. We could define a custom format "phone" for our schema to use it in multiple places:

1
ajv.addFormat('phone', /^[0-9()\-\.\s]+$/);

and then the schema for the phone property would be:

1
{ "type": "string", "format": "phone" }

Task 1

Create a schema that will require the data to be a date (string) or a year (number) and that a year is bigger than or equal to 1976.

Put your answer in the file part1/task1/date_schema.json and run node part1/task1/validate to check it.

Object Validation

In addition to "properties", you can see several other keywords in our user example that apply to objects.

The "required" keyword lists properties that must be present in the object for it to be valid. As you remember, the "properties" keyword doesn't require properties, it only validates them if they are present. "required" complements "properties", allowing you to define which properties are required and which are optional.

If we had this schema:

1
{
2
  "properties": {
3
    "foo": { "type": "string" }
4
  },
5
  "required": ["foo"]
6
}

then all objects without property foo would be invalid.

Please note that this schema still doesn't require our data to be an object—all other data types are valid according to it. To require that our data is an object, we have to add the "type" keyword to it.

Try the example above in the browser.

The "patternProperties" keyword allows you to define schemas according to which the data property value should be valid if the property name matches the regular expression. It can be combined with the "properties" keyword in the same schema.

The feeds property in the user example should be valid according to this schema:

1
{
2
  "type": "object",
3
  "patternProperties": {
4
     "^[A-Za-z]+$": { "type": "boolean" }
5
  },
6
  "additionalProperties": false
7
}

To be valid, feeds should be an object with properties whose names consist only of Latin letters and whose values are boolean.

The "additionalProperties" keyword allows you to either define the schema according to which all other keywords (not used in "properties" and not matching "patternProperties") should be valid, or to prohibit other properties completely, as we did in the feeds property schema above.

In the following example, "additionalProperties" is used to create a simple schema for hash of integers in a certain range:

1
var schema = {
2
  "type": "object",
3
  "additionalProperties" {
4
    "type": "integer",
5
    "minimum": 0,
6
    "maximum": 65535
7
  }
8
};
9
var validate = ajv.compile(schema);
10
validate({a:1,b:10,c:100});    // true
11
validate({d:1,e:10,f:100000}); // false
12
validate({g:1,h:10,i:10.5});   // false
13
validate({j:1,k:10,l:'abc'});  // false

The "maxProperties" and "minProperties" keywords allow you to limit the number of properties in the object. In our user example, the schema for the address property is:

1
{
2
  "type": "object",
3
  "additionalProperties": { "type": "string" },
4
  "maxProperties": 6,
5
  "required": ["street", "postcode", "city", "country"]
6
}

This schema requires that the address is an object with required properties street, postcode, city and country, allows two additional properties ("maxProperties" is 6), and requires that all properties are strings.

"dependencies" is probably the most complex and confusing and the most rarely used keyword, but is a very powerful keyword at the same time. It allows you to define the requirements that the data should satisfy if it has certain properties.

There are two types of such requirements to the object: to have some other properties (it is called "property dependency") or to satisfy some schema ("schema dependency").

In our user example, one of the possible schemas that the user connection should be valid against is this:

1
{
2
  "properties": {
3
    "connType": { "enum": ["relative"] },
4
    "relation": { "type": "string" }
5
  },
6
  "dependencies": {
7
    "relation": ["close"]
8
  }
9
}

It requires that the connType property is equal to "relative" (see the "enum" keyword below) and that if the relation property is present, it is a string.

It does not require that relation is present, but the "dependencies" keyword requires that IF the relation property is present, THEN the close property should be present too.

There are no validation rules defined for the close property in our schema, although from the example we can see that it probably must be boolean. One of the ways we could correct this omission is to change the "dependencies" keyword to use "schema dependency":

1
"dependencies": {
2
  "relation": {
3
    "properties": {
4
      "close": { "type": "boolean" },
5
    },
6
    "required": ["close"]
7
  }
8
}

You can play with the updated user example in the browser.

Please note that the schema in the "relation" property in the "dependencies" keyword is used to validate the parent object (i.e. connection) and not the value of the relation property in the data.

Task 2

Your database contains humans and machines. Using only the keywords that I've explained so far create a schema to validate both of them. A sample human object:

1
{
2
  "human": true,
3
  "name": "Jane",
4
  "gender": "female",
5
  "DOB": "1985-08-12"
6
}

A sample machine object:

1
{
2
  "model": "TX1000",
3
  "made": "2013-08-29"
4
}

Note that it should be one schema to validate both humans and machines, not two schemas.

Put your answer in the file part1/task2/human_machine_schema.json and run node part1/task2/validate to check it.

Hints: use the "dependencies" keyword, and look in the file part1/task2/invalid.json to see which objects should be invalid.

Which objects that probably should be invalid too are not in the invalid.json file?

The main takeaway from this task is the fact that the purpose of validation is not only to validate all valid objects as valid. I've heard this argument many times: "This schema validates my valid data as valid, therefore it is correct." This argument is wrong because you don't need to do much to achieve it—an empty schema will do the job, because it validates any data as valid.

I think that the main purpose of validation is to validate invalid data as invalid, and that's where all the complexity comes from.

Array Validation

There are several keywords to validate arrays (and they apply to arrays only).

"maxItems" and "minItems" require that the array has not more (or not less) than a certain number of items. In the user example, the schema requires that the number of connections is not more than 150.

The "items" keyword defines a schema (or schemas) according to which the items should be valid. If the value of this keyword is an object (as in the user example), then this object is a schema according to which the data should be valid.

If the value of the "items" keyword is an array, then this array contains schemas according to which the corresponding items should be valid:

1
{
2
  "type": "array",
3
  "items": [
4
    { "type": "integer" },
5
    { "type": "string" }
6
  ]
7
}

The schema in the simple example above requires that the data is an array, with the first item that is an integer and the second that is a string.

What about items after these two? The schema above defines no requirements for other items. They can be defined with the "additionalItems" keyword.

The "additionalItems" keyword only applies to the situation in which the "items" keyword is an array and there are more items in the data than in the "items" keyword. In all other cases (no "items" keyword, it is an object, or there are not more items in the data), the "additionalItems" keyword will be ignored, regardless of its value.

If the "additionalItems" keyword is true, it is simply ignored. If it is false and the data has more items than the "items" keyword—then validation fails:

1
const schema = {
2
  "type": "array",
3
  "items": [
4
    { "type": "integer" },
5
    { "type": "string" }
6
  ],
7
  "additionalItems": false
8
};
9
10
const validate = ajv.compile(schema);
11
console.log(validate([1, "foo", 3])); // false

If the "additionalItems" keyword is an object, then this object is a schema according to which all additional items should be valid:

1
const schema = {
2
  "type": "array",
3
  "items": [
4
    { "type": "integer" },
5
    { "type": "string" }
6
  ],
7
  "additionalItems": { "type": "integer" }
8
};
9
10
const validate = ajv.compile(schema);
11
console.log(validate([1, "foo", 3])); // true
12
console.log(validate([1, "foo", 3, 4])); // true
13
console.log(validate([1, "foo", "bar"])); // false

Please experiment with these examples to see how "items" and "additionalItems" work.

The last keyword that applies to arrays is "uniqueItems". If its value is true, it simply requires that all items in the array are different.

Validating the keyword "uniqueItems" can be computationally expensive, so some validators chose not to implement it or to do so only partially.

Ajv has an option to ignore this keyword:

1
const schema = {
2
  "type": "array",
3
  "uniqueItems": true
4
};
5
6
const ajv = Ajv(); // validate uniqueItems
7
ajv.validate(schema, [1, 2, 3]); // true
8
ajv.validate(schema, [1, 2, 2]); // false
9
10
const ajv = Ajv({uniqueItems: false}); // ignore uniqueItems
11
ajv.validate(schema, [1, 2, 3]); // true
12
ajv.validate(schema, [1, 2, 2]); // true

Task 3

One of the ways to create a date object in JavaScript is to pass from 2 to 7 numbers to the Date constructor:

1
const date = new Date(2015, 2, 15); // Sun Mar 15 2015 00:00:00 GMT+0000, month is 0 based
2
const date2 = new Date(year, month0, day, hour, minute, seconds, ms);

You have an array. Create a schema that will validate that this is a valid list of arguments for the Date constructor.

Put your answer in the file part1/task3/date_args_schema.json and run node part1/task3/validate to check it.

"Enum" Keyword

The "enum" keyword requires that the data is equal to one of several values. It applies to all types of data.

In the user example, it is used to define the gender property inside the personal property as either "male" or "female". It is also used to define the connType property in user connections.

The "enum" keyword can be used with any types of values, not only strings and numbers, although it is not very common.

It can also be used to require that data is equal to a specific value, as in the user example:

1
"properties": {
2
  "connType": { "enum": ["relative"] },
3
  ...
4
}

Another keyword supported in the latest versions of JSON Schema is constant:

1
const schema = {
2
  "constant": "relative"
3
};
4
5
const ajv = Ajv({v5: true}); // this options enables v5 keywords
6
const validate = ajv.compile(schema);
7
validate("relative"); // true
8
validate("other"); // false

Compound Validation Keywords

There are several keywords that allow you to define an advanced logic involving validation against multiple schemas. All the keywords in this section apply to all data types.

Our user example uses the "oneOf" keyword to define requirements to the user connection. This keyword is valid if the data successfully validates against exactly one schema inside the array.

If data is invalid according to all schemas in the "oneOf" keyword or valid according to two or more schemas, then the data is invalid.

Let's look more closely at our example:

1
{
2
  ...
3
  "oneOf": [
4
    {
5
      "properties": {
6
        "connType": { "enum": ["relative"] },
7
        "relation": { "type": "string" }
8
      },
9
      "dependencies": {
10
        "relation": ["close"]
11
      }
12
    },
13
    {
14
      "properties": {
15
        "connType": { "enum": ["friend", "colleague", "other"] },
16
        "relation": { "not": {} },
17
        "close": { "not": {} }
18
      }
19
    }
20
  ],
21
  ...
22
}

The schema above requires that user connection is either "relative" (connType property), in which case it may have properties relation (string) and close (boolean), or one of types "friend", "colleague" or "other" (in which case it must not have properties relation and close).

These schemas for user connection are mutually exclusive, because there is no data that can satisfy both of them. So if the connection is valid and has type "relative", there is no point validating it against the second schema—it will always be invalid. Nevertheless, any validator will always be validating data against both schemas to make sure that it is only valid according to one.

There is another keyword that allows you to avoid it: "anyOf". This keyword simply requires that data is valid according to some schema in the array (possibly to several schemas).

In cases such as above, where schemas are mutually exclusive and no data can be valid according to more than one schema, it is better to use the "anyOf" keyword—it will validate faster in most cases (apart from the one, in which the data is valid according to the last schema).

Using "oneOf" in cases where "anyOf" does an equally good job is a very common mistake that negatively affects validation performance.

Our user example would also benefit from replacing "oneOf" with "anyOf".

There are some cases, though, when we really need the "oneOf" keyword:

1
{
2
  "type": "string",
3
  "oneOf": [
4
    { "pattern": "apple" }
5
    { "pattern": "orange" }
6
  ]
7
}

The schema above will successfully validate strings that mention oranges or apples, but not both (and there do exist strings that can mention both). If that's what you need, then you need to use "oneOf".

Comparing with boolean operators, "anyOf" is like boolean OR and "oneOf" is like XOR (exclusive OR). The fact that JavaScript (and many other languages) don't define operators for exclusive OR shows that it is rarely needed.

There is also the keyword "allOf". It requires that the data is valid according to all schemas in the array:

1
{
2
  "allOf": [
3
    { "$ref": "http://mynet.com/schemas/feed.json#" },
4
    { "maxProperties": 5 }
5
  ]
6
}

The "$ref" keyword allows you to require that data is valid according to the schema in another file (or some part of it). We will be looking at it in the second part of this tutorial.

Another mistake is to put more than absolutely necessary inside schemas in the "oneOf", "anyOf" and "allOf" keyword. For example, in our user example, we could put inside "anyOf" all requirements that the connection should satisfy.

We also could have unnecessarily complicated the example with apple and oranges:

1
{
2
  "oneOf": [
3
    { "type": "string", "pattern": "apple" }
4
    { "type": "string", "pattern": "orange" }
5
  ]
6
}

Another "logical" keyword is "not". It requires that the data is NOT valid according to the schema that is the value of this keyword.

For example:

1
{
2
  "type": "string",
3
  "not": {
4
    "pattern": "apple"
5
  }
6
}

The schema above would require that the data is a string that does not contain "apple".

In the user example, the "not" keyword is used to prevent some properties from being used in one of the cases in "oneOf", although they are defined:

1
{
2
  "properties": {
3
    "connType": { "enum": ["friend", "colleague", "other"] },
4
    "relation": { "not": {} },
5
    "close": { "not": {} }
6
  }
7
}

The value of the "not" keyword in the example above is an empty schema. An empty schema will validate any value as valid, and the "not" keyword will make it invalid. So the schema validation will fail if the object has the property relation or close. You can achieve the same with the combination of "not" and "required" keywords.

Another use of the "not" keyword is to define the schema that requires that an array contains an item that is valid according to some schema:

1
{
2
  "not": {
3
    "items": {
4
      "not: {
5
        "type": "integer",
6
        "minimum": 5
7
      }
8
    }
9
  }
10
}

The schema above requires that the data is an array and it contains at least one integer item greater than or equal to 5.

V5 proposals include the keyword "contains" to satisfy this requirement.

1
var schema = {
2
  "type": "array",
3
  "contains": {
4
    "type": "integer",
5
    "minimum": 5
6
  }
7
};
8
9
const ajv = Ajv({v5: true}); // enables v5 keywords
10
const validate = ajv.compile(schema);
11
validate([3, 4, 5]); // true
12
validate([1, 2, 3]); // false

Task 4

You have a database of users that all match schema from the user example. Create a schema according to which only users that satisfy all these criteria will be valid:

  • unmarried men younger than 21 or older than 60 years
  • have 5 or less connections
  • subscribe to 3 or less feeds

Put your answer in the file part1/task4/filter_schema.json and run node part1/task4/validate to check it.

The test data is simplified, so please do not use the "required" keyword in your schema.

Keywords Describing the Schema

Some keywords used in the user example do not directly affect validation, but they describe the schema itself.

The "$schema" keyword defines the URI of the meta-schema for the schema. The schema itself is a JSON document, and it can be validated using JSON-schema. A JSON-schema that defines any JSON-schema is called a meta-schema. The URI for the meta-schema for draft 2020-12 of the JSON-schema standard is https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-01.

If you extend the standard, it is recommended that you use a different value of the "$schema" property.

"id" is the schema URI. It can be used to refer to the schema (or some part of it) from another schema using the "$ref" keyword—see the second part of the tutorial. Most validators, including Ajv, allow any string as "id". According to the standard, the schema id should be a valid URI that can be used to download the schema.

You can also use "title" and "description" to describe the schema. They are not used during the validation. Both these keywords can be used on any level inside the schema to describe some parts of it, as is done in the user example.

Final Task

Create an example of a user record that when validated with the example user schema will have 8 or more errors.

Put your answer in the file part1/task5/invalid_user.json and run node part1/task5/validate to check it.

What is still very wrong with our user schema?

Extra Section: An Alternative to JSON Schema

You might have noticed that JSON Schema is not the only schema format that Ajv supports. Ajv also supports JSON Type Definition, an alternative to JSON Schema that is simpler and more concise. For example, if  we were trying to describe a patch statement that complies with JSON Patch using JSON Typedef, the schema would look something like this:

1
{
2
    "definitions": {
3
        "addop": {
4
            "properties": {
5
                "op": {
6
                    "type": "string",
7
                    "enum": [
8
                        "add",
9
                        "replace",
10
                        "test"
11
                    ]
12
                },
13
                "value": {},
14
                "path": {
15
                    "type": "string"
16
                }
17
            }
18
        },
19
        "removeop": {
20
            "properties": {
21
                "op": {
22
                    "type": "string",
23
                    "enum": [
24
                        "remove"
25
                    ]
26
                },
27
                "path": {
28
                    "type": "string"
29
                }
30
            }
31
        },
32
        "moveop": {
33
            "properties": {
34
                "op": {
35
                    "type": "string",
36
                    "enum": [
37
                        "move",
38
                        "copy"
39
                    ]
40
                },
41
                "path": {
42
                    "type": "string"
43
                },
44
                "from": {
45
                    "type": "string"
46
                }
47
            }
48
        }
49
    },
50
    "elements": {
51
        "discriminator": "op",
52
        "mapping": {
53
            "add": {
54
                "ref": "addop"
55
            },
56
            "replace": {
57
                "ref": "addop"
58
            },
59
            "test": {
60
                "ref": "addop"
61
            },
62
            "move": {
63
                "ref": "moveop"
64
            },
65
            "copy": {
66
                "ref": "moveop"
67
            }
68
        }
69
    }
70
}

Then, to validate data with the schema using Ajv, I would do this:

1
"use strict";
2
3
if (process.env.NODE_ENV != "test") return;
4
5
const Ajv = require("ajv/dist/jtd");
6
const assert = require("assert");
7
8
const patchData = require("./data");
9
const patchSchema = require("./schema");
10
11
const ajv = Ajv({ allErrors: true });
12
13
const validate = ajv.compile(patchSchema);
14
assert(test(validate));
15
16
console.log("Patch schema OK");
17
18
function test(validate) {
19
  const valid = validate(userData);
20
21
  if (valid) {
22
    console.log("Patch data is valid!");
23
  } else {
24
    console.log("Patch data is INVALID!");
25
    console.log(validate.errors);
26
  }
27
28
  return valid;
29
}

The code is almost identical to using JSON Schema, except that you have to import ajv/dist/jtd.

What's Next?

By now you know all the validation keywords defined by the standard, and you should be able to create quite complex schemas. As your schemas grow, you will be reusing some parts of them. Schemas can be structured into multiple parts and even multiple files to avoid repetition. We will be doing this in the second part of the tutorial.

We also will:

  • use a schema to define default values
  • filter additional properties from the data
  • use keywords included in the proposals for version 5 of the JSON-schema standard
  • define new validation keywords
  • compare existing JSON-schema validators

Thanks for reading!

This post has been updated with contributions from Jacob Jackson. Jacob is a web developer, technical writer, a freelancer, and an open-source contributor.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.