Streamlining Form Validation with JSON Schema for Front-End and Back-End

Written by dsitdikov | Published 2023/03/17
Tech Story Tags: form-validation | json | json-schema | frontend | backend | web-development | webdev | data-management

TLDRIn our app, we have around 60 form fields among decades of modals. We are in the early stages of development, and it means that the power of change can definitely affect us. These circumstances led us to find solutions that satisfy the following requirements. One dedicated file with validation rules for all consumers: services, web apps, mobile apps, etc. Support for conditional validation. Understandable language for product analytics.via the TL;DR App

In our app, we have around 60 form fields among decades of modals, and I am sure that this is not the final number as we work in multinational legal and finance business domains. Because of that, we have to validate a lot of form fields based on a variety of conditions (such as country).

Moreover, we are in the early stages of development, and it means that the power of change can definitely affect us.

These circumstances led us to find solutions that satisfy the following requirements:

  1. One source of the truth. In other words, one dedicated file with validation rules for all consumers: services, web apps, mobile apps, etc. Because on the opposite side after successful front-end validation service can reject a request because of invalid incoming data.

  2. Support for 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. The ability to show error messages which are clear for users.


Solution

We decided to use JSON Schema (draft 7). It served 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 a basic example. We need to verify just one field: it should be required and follow an email regular expression.


Our model is:

{
   "email": "Steve"
}

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 [email protected]"
       }
   },
   "required": ["email"]
}

Conditional fields

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


Let’s have a look at the concrete 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 the country:

{
   "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 ifblock. 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"
                   }
               }
           }
       }
   ]
}

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.


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"
}

And validation schema:

{
   "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. It is platform-independent. In other words, you can use it in front-end apps with any framework and in Node.JS. Apart from that, ajv makes it 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);

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"
    }
]

And depending 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 basic ones.

Thanks for reading! ✨


Also published here.


Written by dsitdikov | 6+ years of web development experience in tech. companies
Published by HackerNoon on 2023/03/17