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:
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.
Support for conditional validation: for instance, unique rules of legal entity fields for each country.
Understandable language for product analytics. To be able to amend rules without engineers.
The ability to show error messages which are clear for users.
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.
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"]
}
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.
For the United Kingdom, it can only be: GB000000000(000), GBGD000 or GBHA000
For Russia: exactly 9 digits and nothing else
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"
}
}
}
}
]
}
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:
Our model is the following:
{
"swiftBic": "",
"sortCode": "402030"
}
And validation schema:
{
"type": "object",
"anyOf": [
{
"required": ["swiftBic"]
},
{
"required": ["sortCode"]
}
]
}
JSON Schema is supported by many languages. However, the most investigated by me was the JavaScript version.
We took ajv library as the
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.
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.