Single place of form validation rules for clients and services with JSON Schema
Tags: validation, forms, architecture, javascript
Published: 2022-01-17 20:17:40

## Introduction

In our app, we have around 60 form fields among decades of modals and I am sure that this is not the final number. We work in multinational legal and finance business domains. Because of that, we have to validate a lot of form fields based on some conditions (such as country). Moreover, we are in the early stages of development and it means that the power of changes definitely can affect us.

These circumstances led us to find the solution which has to satisfy these requirements:
1. It should be one source of the truth. In other words, one dedicated file with validation rules for all consumers: services, web apps, mobile apps, etc. Because in the opposite case after successful front-end validation service can reject a request because of invalid incoming data
2. It supports conditional validation: for instance, unique rules of legal entity fields for each country.
3. Understandable language for product analytics. To be able to amend rules without engineers.
4. Ability to show error messages which are clear for users

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cd5xhm8mghbynn5q4iy3.jpg)

## Solution

We decided to use JSON Schema (draft 7). It closed our needs. In a nutshell, it's standard represented as JSON which contains a set of rules for some JSON objects. Now we're going to overview the most common and useful validation patterns.

### Basic

Let's start with the basic example. We need to verify just one field: it should be required and follow an email regular expression.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nivua1e5vo84egdvijzp.jpg)

Our model is:

```json
{
   "email": "Steve"
}
```

and our validation schema is the following:
```json
{
   "type": "object",
   "properties": {
       "email": {
           "type": "string",
           "pattern": "(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])",
           "errorMessage": "Can be only in name@domain.com"
       }
   },
   "required": ["email"]
}
```

### Conditional fields

Sometimes we need to apply some validation rules depending on the values in the other selected fields.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oqnnm4un8o3l5io6hj78.jpg)

Let’s have a look at concreate case. Here, each country should apply unique validation for a VAT number.

1. For the United Kingdom, it can only be: GB000000000(000), GBGD000 or GBHA000
2. For Russia: exactly 9 digits and nothing else
3. For other countries, we don’t apply any validations for now. (as we’re going to extend this piece by piece)

The model is a bit more complicated. Now we have country:
```json
{
   "name": "Samsung Ltd.",
   "country": {
       "id": "GB",
       "name": "United Kingdom"
   },
   "vatNumber": "314685"
}
```

To perform conditional validation we’re going to use **allOf** construction as well as **if** and **then** blocks. Please, pay attention to the **required** field in the **if** block. It has to be here. Otherwise, it won’t work.

```json
{
   "type": "object",
   "properties": {
       "name": {
           "type": "string"
       },
       "vatNumber": {
           "type": "string"
       }
   },
   "required": [
       "vatNumber",
       "name"
   ],
   "allOf": [
       {
           "if": {
               "properties": {
                   "country": {
                       "properties": {
                         "id": {"const": "GB"}
                       }
                   }
               },
               "required": ["country"]
           },
           "then": {
               "properties": {
                   "vatNumber": {
                       "pattern": "^GB([\\d]{9}|[\\d]{12}|GD[\\d]{3}|HA[\\d]{3})$",
                       "errorMessage": "Can be GB000000000(000), GBGD000 or GBHA000"
                   }
               }
           }
       },
       {
           "if": {
               "properties": {
                   "country": {
                       "properties": {
                           "id": {"const": "RU"}
                       }
                   }
               },
               "required": ["country"]
           },
           "then": {
               "properties": {
                   "vatNumber": {
                       "pattern": "^[0-9]{9}$",
                       "errorMessage": "Can be only 9 digits"
                   }
               }
           }
       }
   ]
}
```

### Either one or all

Sometimes we need to fill at least one field. As a real-world example, to perform payments in the UK you should know the BIC/SWIFT or sort code numbers of a bank. If you know both — excellent! But at least one is mandatory.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2cd45s7dmiem00dnjuj9.jpg)

To do that we will use **anyOf** construction. As you noticed this is the second keyword after **allOf**. Just to clarify all of them:
1. allOf — ALL statements should be valid
2. oneOf — ONLY ONE statement should be valid. If more or nothing it fails
3. anyOf — ONE OR MORE statements should be valid

Our model is the following:
```json
{
   "swiftBic": "",
   "sortCode": "402030"
}
```

And validation schema:
```json
{
   "type": "object",
   "anyOf": [
       {
           "required": ["swiftBic"]
       },
       {
           "required": ["sortCode"]
       }
   ]
}
```

### Implementation on JavaScript

JSON Schema is supported by many languages. However, the most investigated by me was the JavaScript version.

We took ajv library as the [fastest one](https://github.com/ajv-validator/ajv#performance). It is platform-independent. In other words, you can use it as in front-end apps with any frameworks and in Node.JS. 

Apart from that, avj makes possible to use custom error messages. Because, unfortunately, they are not supported by standards.

Before we start, we need to add 2 dependencies: **ajv** and **ajv-errors**.

```js
import Ajv from 'ajv';
import connectWithErrorsLibrary from 'ajv-errors';

const ajv = new Ajv({
   // 1. The error message is custom property, we have to disable strict mode firstly
   strict: false,
   // 2. This property enables custom error messages
   allErrors: true
});
// 3. We have to connect an additional library for this
connectWithErrorsLibrary(ajv);

// 4. Our model
const dto = { dunsNumber: 'abc' };

// 5. Validation schema
const schema = {
   type: 'object',
   properties: {
       dunsNumber: {
           type: 'string',
           pattern: '^[0-9]{9}$',
           errorMessage: 'Can be only 9 digits'
       }
   },
   required: ['dunsNumber']
};

// 6. Set up validation container
const validate = ajv.compile(schema);

// 7. Perform validation.
// ... It's not straightforward, but the result will be inside the "error" property
validate(dto);

console.log('field error:', validate.errors);
```

As the result we’ll have:
```json
[
    {
        "instancePath": "/dunsNumber",
        "schemaPath": "#/properties/dunsNumber/errorMessage",
        "keyword": "errorMessage",
        "params": {
            "errors": [
                {
                    "instancePath": "/dunsNumber",
                    "schemaPath": "#/properties/dunsNumber/pattern",
                    "keyword": "pattern",
                    "params": {
                        "pattern": "^[0-9]{9}$"
                    },
                    "message": "must match pattern \"^[0-9]{9}$\"",
                    "emUsed": true
                }
            ]
        },
        "message": "Can be only 9 digits"
    }
]
```

and depends on our form implementation we can get the error and put it inside the invalid fields.

### Conclusion

To perform the validation which is described in one single place we used JSON Schema. Moreover, we came across the cases like conditional validations, selective validation and the basic one.

Thanks for reading! ✨