DEV Community

Cover image for Single place of form validation rules for clients and services with JSON Schema
Daniil Sitdikov
Daniil Sitdikov

Posted on

Single place of form validation rules for clients and services with JSON Schema

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

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

Our model is:

{
   "email": "Steve"
}
Enter fullscreen mode Exit fullscreen mode

and our validation schema is the following:

{
   "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"]
}
Enter fullscreen mode Exit fullscreen mode

Conditional fields

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

Image description

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:

{
   "name": "Samsung Ltd.",
   "country": {
       "id": "GB",
       "name": "United Kingdom"
   },
   "vatNumber": "314685"
}
Enter fullscreen mode Exit fullscreen mode

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.

{
   "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"
                   }
               }
           }
       }
   ]
}
Enter fullscreen mode Exit fullscreen mode

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

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:

{
   "swiftBic": "",
   "sortCode": "402030"
}
Enter fullscreen mode Exit fullscreen mode

And validation schema:

{
   "type": "object",
   "anyOf": [
       {
           "required": ["swiftBic"]
       },
       {
           "required": ["sortCode"]
       }
   ]
}
Enter fullscreen mode Exit fullscreen mode

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. 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.

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);
Enter fullscreen mode Exit fullscreen mode

As the result we’ll have:

[
    {
        "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"
    }
]
Enter fullscreen mode Exit fullscreen mode

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! ✨

Top comments (0)