Data Validation Practise for Frontend
If you want to know how to validate forms in Vue - this article, is not for you. You should use some standard Vue plugins such as vuelidate
My advice: validation schema must be placed in the component that will submit the form.
We often create software that depends on data from some third side(ex. API calls, Backend, Parent Component, ...), you need to be ready that data you get can have any shape and content. So we need to validate data, that we take from other places.
Contents
- Solution Requirements
- Solution
- Validation of types
- Custom validation rules
- Deep validation
- Fixing of invalid data
- Tracking
- Additional Possibilities
- Other Solutions
- Contacts
Solution Requirements
For almost all solution there are more or less useful solutions. And for our problem we set these goals to be achieved:
- Validation of types(number, object, array, string, null, undefined,...)
- Custom validation rules;
- Deep validation;
- Fixing of invalid data:
- set default value;
- omit invalid.
- Tracking:
- messages,
- errors;
- Clear code
- Readable
- Modifiable
Solution
As one of the solutions that we can use to achieve this goals is quartet
library.
These library based on this validation definition:
"To validate" is to prove that some data is acceptable for using.
From the definition we see that validation has only two possible results: "data is acceptable" and "data is not acceptable". In javascript we represent this value in such way:
Result | JS value |
---|---|
Data is acceptable | true |
Data isn't acceptable | false |
Let's see how do we use quartet
to achieve goals described above.
Validation of Types
For testing types we can use default registered validators and custom functions.
// Import library
import quartet from 'quartet'
const v = quartet()
v
- is a function that transforms schema into validation function. It takes two arguments
- Validation schema (required)
- Custom error (optional)
Validation schema is one of:
- validation function (function that returns
true
orfalse
)- names of registered validation functions
- array of schemas of validation alternatives.
- object like
{ key1: schemaForKey1, key2: schemaForKey2, ... }
Custom error is any javascript value(except
undefined
) or function that returns any javascript value. This value will be treated as explanation of schema validation error. And will stored inv.explanation
. Example of usage see in Tracking section.
Validation of Numbers
const isNumber = v('number') // returns typeof value === 'number'
isNumber(0) // true
isNumber(-1) // true
isNumber(1) // true
isNumber(1.2) // true
isNumber(NaN) // true
isNumber(Infinity) // true
isNumber(-Infinity) // true
isNumber('1') // false
isNumber(new Number(123)) // false
Checking of finite numbers (without NaN, Infinity, -Infinity)
// Lets put all values into array
// and find all values that are finite numbers
const numberLikeValues = [0, -1, 1, 1.2, NaN, Infinity, -Infinity, '1', new Number(123)]
// v('filter') is the same function as: value => Number.isFinite(value))
numberLikeValues.filter(v('finite')) // [0, -1, 1, 1.2]
Checking of integer numbers
// v('safe-integer') is the same function as: value => Number.isSafeInteger(value))
numberLikeValues.filter(v('safe-integer')) // [0, -1, 1]
Also we can check number sign:
// v('positive') is the same function as: x => x > 0
numberLikeValues.filter(v.and('positive', 'finite')) // [1, 1.2]
v.and(schema, schema2, schema3, ...)
means that validated value must matchschema
ANDschema2
ANDschema3
and so on.
// v('negative') is the same function as: x => x < 0
numberLikeValues.filter(v.and('negative', 'number')) // [-1, -Infinity]
// v('negative') is the same function as: x => x < 0
numberLikeValues.filter(v.and('non-positive', 'finite')) // [0, -1]
numberLikeValues.filter(v.and('non-negative', 'safe-integer')) // [0, 1]
Also there is methods that returns number validation functions:
-
v.min(minValue)
; -
v.max(maxValue)
; -
v.enum(value, value2, ...)
checks if validated value is one of passed values.
Let's use them to test rating value:
// v.min(minValue) for numbers is the same function as: x => x >= minValue
// v.max(minValue) for numbers is the same function as: x => x <= maxValue
const isRating = v.and('safe-integer', v.min(1), v.max(5))
isRating(1) // true
isRating(5) // true
isRating('2') // false
isRating(0) // false
isRating(6) // false
The same, but with using of v.enum
// v.enum(...values) is the same function as: x => values.includes(x)
const isRating2 = v.enum(1,2,3,4,5)
isRating2(1) // true
isRating2(5) // true
isRating2('2') // false
isRating2(0) // false
isRating2(6) // false
Validation of strings
const stringLikeObjects = [
'',
'123',
new String('123'),
Number('string')
]
// lets find only strings
stringLikeObjects.filter(v('string')) // ['', '123']
Also like for numbers there is additional registered validator for strings: 'not-empty'
:
stringLikeObjects.filter(v.and('not-empty', 'string')) // ['123']
There is also methods for creating string validation functions:
- v.regex(regularExpression: RegExp);
- v.min(minLength: number);
- v.max(minLength: number).
Let's use them to check password (stupid passwords only)
const v = require('quartet')()
const isValidPassword = v.and(
'string', // typeof x === 'string'
v.min(8), // length >= 8
v.max(24), // length <= 24
v.regex(/^[a-zA-Z0-9]+$/), // must contain only letters and digits
v.regex(/[a-z]/), // at least one small letter
v.regex(/[A-Z]/), // at least one big letter
v.regex(/[0-9]/) // at least one digit
)
console.log(isValidPassword('12345678')) // false
console.log(isValidPassword('12345678Password')) // true
Validation of Other Types
You can use next registered validation functions in your validation schemas to check type.
name | condition |
---|---|
'boolean' | x => typeof x === 'boolean' |
'null' | x => x === null |
'undefined' | x => x === undefined |
'nil' | `x => x === null |
'object' | {% raw %}x => typeof x === 'object'
|
'object!' | x => typeof x === 'object' && x !== null |
'array' | x => Array.isArray(x) |
'symbol' | x => typeof x === 'symbol' |
'function' | x => typeof x === 'function' |
Alternatives
Sometimes there is need to validate data that can be different types.
You can use schema of alternatives to get such behavior:
// It is works exactly as OR operator in JS,
// if some of alternatives - true, it will return true immediately
v(['number', 'string'])(1) // true
v(['number', 'string'])('1') // true
v(['number', 'string'])(null) // false
v(['number', 'string'])(new String(123)) // false
v(['number', 'string', 'object'])(null) // true
v(['number', 'string', 'object'])(new String(123)) // true
Custom validation rules
As it was said before: validation function is one of
valid schemas. If you want to add your own rule - you just need to use your validation function as a schema.
const isPrime = n => {
if (n < 2) return false
if (n === 2 || n === 3) return true
if (n % 2 === 0 || n % 3 === 0) return false
for (let i = 5, j = 7; i * i <= n; i+=6, j+=6) {
if (n % i === 0) return false
if (n % j === 0) return false
}
return true
}
const isPrimeAndNotLessThan100 = v.and(
'safe-integer',
v.min(100),
isPrime // validation function
)
isPrimeAndNotLessThan100(512) // false, 512 is NOT a prime number
isPrimeAndNotLessThan100(523) // true, 523 > 100, 523 is a prime number
Deep validation
Deep validation means validation of not primitive data structures.
Data structure can be accepted only if it has right type and all parts of it are valid.
The most popular data structures are object and array.
Deep Validation of Object
For validation of object quartet
uses object schema.
Object schema is an object of a such structure
{ key1: schema1, key2: schema2, // ... // And if you need to validate other properties of object // You can use v.rest(schema) method (it returns an object that must be spreaded into object schema) ...v.rest(schemaAppliedToOtherValues) }
Example:
// `v` treats object as an object
const isWorkerValid = v({
name: v.and('not-empty', 'string'),
age: v.and('positive', 'safe-integer)',
position: v.enum(
'Frontend Developer',
'Backend Developer',
'QA',
'Project manager',
'Grandpa'
),
salary: v.and('positive', 'finite'),
project: v.enum(
'Shoutout',
'FMEvents',
'Jobla.co'
),
// Any field can be object too
skills: {
JS: 'boolean',
HTML: 'boolean',
CSS: 'boolean',
...v.rest('boolean') // other keys must be boolean too
}
})
Let's validate some object with using of this validation function
const worker = {
name: 'Max',
age: 31,
position: 'Grandpa',
salary: Math.random() * 3000,
project: 'Jobla.co',
skills: {
JS: true,
HTML: true,
CSS: true,
'C++ advanced': false,
'GPU programming': false
}
}
isWorkerValid(worker) // true
There is additional methods for dictionary object validation:
-
v.dictionaryOf(schema)
- checks values of object; -
v.keys(schema)
- checks keys of object; -
v.rest(schema)
- if other properties will be present - they will be validated with using of the schema.
Example: Validation of dictionary object
const lowLettersDict = {
A: 'a',
B: 'b',
C: 'c'
}
const isValidLettersDict = v.and(
v.keys(v.regex(/^[A-Z]$/)),
v.dictionaryOf(v.regex(/^[a-z]$/))
)
console.log(isValidLettersDict(lowLettersDict))
Let's check if keys correspond values with using of
custom validation function
// second parameter of all validation function is
// {
// key: string|number,
// parent: any
// }
// (if the parent is present)
function isValueValid (value, { key }) {
return /^[A-Z]$/.test(key) // upperCased key
&& /^[a-z]$/.test(value) // lowerCased value
&& value === key.toLowerCase() // correspond each other
}
const isValidLettersDict2 = v.dictionaryOf(isValueValid)
console.log(isValidLettersDict2(lowLettersDict)) // true
console.log(isValidLettersDict2({ A: 'b' })) // false, NOT CORRESPONDS
console.log(isValidLettersDict2({ b: 'b' })) // false, b is not UpperCased
console.log(isValidLettersDict2({ B: 'B' })) // false, B is not LowerCased
Deep Validation of Array
For deep validation of array we can use v.arrayOf(schema)
method.
const arr = [1,2,3,4]
const invalidArrOfNumbers = [1,2,'3','4']
const isArrayValid = v.arrayOf('number')
isArrayValid(arr) // true
isArrayValid(invalidArrOfNumbers) // false
Also, we can combine array validation schema with object schemas
const isValidPointArray = v.arrayOf({
x: 'finite',
y: 'finite'
})
isValidPointArray([
{ x: 1, y: 2},
{ x: -1, y: 3},
{ x: 0, y: 0},
]) // true
And another way: object with array property:
const student = {
name: 'Valera',
grades: ['A', 'B', 'C','A', 'D', 'F']
}
const isStudentValid = v({
name: 'string',
grades: v.arrayOf(v.enum('A', 'B', 'C', 'D', 'E', 'F'))
})
isStudentValid(student) // true
Fixing of invalid data:
What if some validation errors we can fix. For example, we can replace invalid data with empty valid data. Also, sometimes we can omit invalid data. Or in rare keys - we should try to transform invalid data to valid.
In quartet
there are methods for such task. Main method is
v.fix(invalidValue) => validValue
This method is used for applying all fixes that were collected during the validation. It doesn't change invalidValue
but returns new value with applied fixes.
Methods v.default(schema, defaultValue)
, v.filter(schema)
and v.addFix(schema, fixFunction)
are decorators of validators. It means that they return new validation function that works exactly as passed schema, but with side effect of collecting of fixes.
Decorator | Fix effect, after calling v.fix
|
---|---|
v.default |
Replace value with defaultValue |
v.filter |
Removes value from parent |
v.addFix |
Custom fixFunction mutates parents of the value to fix an error |
Example:
Let's create several validation functions with different effects.
const arr = [1,2,3,4,'5','6','7']
// Replaces all not numbers with 0
const isArrayValid = v.arrayOf(
v.default('number', 0)
)
// Removes all not numbers from parent(array)
const isArrayValidFilter = v.arrayOf(
v.filter('number')
)
// This function will be called on value in the clone of invalid data
// So this mutations - are safe.
function castToNumber(invalidValue, { key, parent }) {
parent[key] = Number(invalidValue)
}
// casts all not numbers into numbers
const isArrayValidFix = v.arrayOf(
v.addFix('number', castToNumber)
)
Let's use them to validate arr
:
v.clearContext() // remove all fixes stored in `v`
isArrayValid(arr) // false
const validArr = v.fix(arr)
console.log(validArr) // [1,2,3,4,0,0,0]
v.clearContext() // remove previous fixes
isArrayValidFilter(arr) // false
const validArr2 = v.fix(arr) // [1,2,3,4]
v() // same as v.clearContext()
isArrayValidFix(arr) // false
const validArr3 = v.fix(arr) // [1,2,3,4,5,6,7]
// arr is not mutated
console.log(arr) // [1,2,3,4,'5','6','7']
NOTE: if there is "fix effect" on parent "fix effect" of children will not be applied(if fix effect on parent is not "filter effect").
It means we should use such rule: if there is a "fix effect", it must fix all invalidation of the value it must fix.
Example:
const isObjectValid = v({
arr: v.default( // will be applied
v.arrayOf(
v.filter('number') // will not be applied
),
[] // if there will be any not number - all array will be replaced with []
)
})
const invalidObj = {
arr: [1,2,3,'4']
}
v()
isObjectValid(invalidObj)
const validObj = v.fix(invalidObj) // { arr: [] }
Also there is
v.hasFixes
method: it returnstrue
- if some fixes were collected, and ready to be applied. Returnsfalse
otherwise.
Tracking
Sometimes we need not only to check if a value is not valid,
But to get an explanation, and possibly to send this explanation to
the user, or to the logger etc.
In quartet
we use explanations for it.
Explanation - any JS value(except undefined) you want that describes invalidation error.
We use the second parameter of v
to add the effect of storing explanation, it can be either:
- explanation;
- a function that returns explanation.
We use them to collect error messages and errors into v.explanation
array.
Messages
Sometimes we need only data to show to the user. And string explanation of the error is very useful.
Example:
const isValidPerson = v.and(
v('object!', 'Person data structure is not an object'),
{
name: v.and(
// required, checks if parent has such property
v('required', 'name field is absent'),
v('string', 'Person name is not a string'),
v('not-empty', 'Person with empty name, really?')
),
age: v.and(
v('required', 'age field is absent'),
v('safe-integer', 'Person age is not an integer number'),
v(v.min(18), 'Person has is not an appropriate age'),
v(v.max(140), `It was just a healthy food`)
)
}
)
Let's use this schema to validate several persons
v.clearContext() // or v()
isValidPerson(null) // false
console.log(v.explanation) // ['Person data structure is not an object']
v.clearContext()
isValidPerson({}) // false
console.log(v.explanation)
/*
* [
* 'Name field is absent',
* 'age field is absent'
* ]
*/
v() // same as v.clearContext()
isValidPerson({ name: '', age: 969 })
console.log(v.explanation)
/**
* [
* 'Person with empty name, really?',
* 'It was just a healthy food'
* ]
*/
We can calculate explanation based on the invalidValue and it's parents.
Example:
const isValidPerson = v.and(
v('object!', 'Person data structure is not an object'),
{
name: v.and(
v('required', 'name field is absent'),
v('string', 'Person name is not a string'),
v('not-empty', 'Person with empty name, really?')
),
age: v.and(
v('required', 'age field is absent'),
v('safe-integer', 'Person age is not an integer number'),
v(v.min(18), age => `Your age: ${age} is to small`),
v(v.max(140), age => `Your age: ${age} is to big`)
)
}
)
v() // same as v.clearContext()
isValidPerson({ name: '', age: 969 })
console.log(v.explanation)
/**
* [
* 'Person with empty name, really?',
* 'Your age: 969 is to big'
* ]
*/
Errors
The same way we use strings we can use objects as an explanation.
// Util for calculating code errors.
// If you want you can create your own type of errors.
const invalidValueToError = code => invalidValue => ({
invalidValue,
code
})
It will be useful to add some error codes.
We can use them to get messages sent to the user and other.
// Error Codes
const CODE = {
PERSON_IS_NOT_AN_OBJECT: 'PERSON_IS_NOT_AN_OBJECT',
NAME_ABSENT: 'NAME_ABSENT',
NAME_IS_NOT_STRING: 'NAME_IS_NOT_STRING',
NAME_IS_EMPTY: 'NAME_IS_EMPTY',
AGE_ABSENT: 'AGE_ABSENT',
AGE_NOT_INTEGER: 'AGE_NOT_INTEGER',
AGE_TO_SMALL: 'AGE_TO_SMALL',
AGE_TO_BIG: 'AGE_TO_BIG'
}
Schema with added using of the invalidValueToError
function that returns function that calculates error explanation.
const isValidPerson = v.and(
v('object!', invalidValueToError(CODE.PERSON_IS_NOT_AN_OBJECT)),
{
name: v.and(
v('required', invalidValueToError(CODE.NAME_ABSENT)),
v('string', invalidValueToError(CODE.NAME_IS_NOT_STRING)),
v('not-empty', invalidValueToError(CODE.NAME_IS_EMPTY))
),
age: v.and(
v('required', invalidValueToError(CODE.AGE_ABSENT)),
v('safe-integer', invalidValueToError(CODE.AGE_NOT_INTEGER)),
v(v.min(18), invalidValueToError(CODE.AGE_TO_SMALL)),
v(v.max(140), invalidValueToError(CODE.AGE_TO_BIG))
)
}
)
Let's check some values and see what is stored in explanation
Not an object
v()
isValidPerson(null)
console.log(v.explanation)
//[
// {
// invalidValue: null,
// code: 'PERSON_IS_NOT_AN_OBJECT'
// }
//]
required fields explanation
v()
isValidPerson({})
console.log(v.explanation)
//[
// {
// invalidValue: undefined,
// code: 'NAME_ABSENT'
// },
// {
// invalidValue: undefined,
// code: 'NAME_ABSENT'
// }
//]
not valid values
v()
isValidPerson({ age: 963, name: '' })
console.log(v.explanation)
//[
// {
// invalidValue: '',
// code: 'NAME_IS_EMPTY'
// },
// {
// invalidValue: 963,
// code: 'AGE_TO_BIG'
// }
//]
All Together
Rarely, but it's possible to use explanations and fixes at one time.
For such goals, there is v.fromConfig
method. That takes the config of the validation and returns validation function that has all set properties.
Example:
This is still the same
const invalidValueToError = code => invalidValue => ({
invalidValue,
code
})
// Error Codes
const CODE = {
PERSON_IS_NOT_AN_OBJECT: 'PERSON_IS_NOT_AN_OBJECT',
NAME_ABSENT: 'NAME_ABSENT',
NAME_IS_NOT_STRING: 'NAME_IS_NOT_STRING',
NAME_IS_EMPTY: 'NAME_IS_EMPTY',
AGE_NOT_VALID: 'AGE_NOT_VALID'
}
Add using of v.fromConfig
const isValidPerson = v.and(
v.fromConfig({
validator: 'object!',
// explanation if not object
explanation: invalidValueToError(CODE.PERSON_IS_NOT_AN_OBJECT),
// If not valid store default fix (calculate default value)
default: () => ({ name: 'unknown' })
}),
{
// if several configs are passed, validations will be combined with `v.and`
name: v.fromConfig(
{
validator: 'required',
default: 'a',
explanation: invalidValueToError(CODE.NAME_ABSENT)
},
{
validator: 'string',
default: 'b',
explanation: invalidValueToError(CODE.NAME_IS_NOT_STRING)
},
{
validator: 'not-empty',
default: 'c',
explanation: invalidValueToError(CODE.NAME_IS_EMPTY)
}
),
age: v.fromConfig(
{
validator: 'safe-integer',
filter: true,
explanation: invalidValueToError(CODE.AGE_NOT_VALID)
},
{
validator: v.min(18),
default: 18,
explanation: invalidValueToError(CODE.AGE_NOT_VALID)
},
{
validator: v.max(140),
default: 90,
explanation: invalidValueToError(CODE.AGE_NOT_VALID)
}
)
}
)
null object
v()
const value = null
const test1 = isValidPerson(value)
const explanation = v.explanation
const fixedValue = v.fix(value)
console.log({
value, // null
test1, // false
explanation, // [{ invalidValue: null, code: 'PERSON_IS_NOT_AN_OBJECT' }]
fixedValue // { name: 'unknown' }
})
empty object
v()
const value2 = {}
const test2 = isValidPerson({})
const explanation2 = v.explanation
const fixedValue2 = v.fix(value2)
console.log({
value2, // {}
test2, // false
// [
// { invalidValue: undefined, code: 'NAME_ABSENT' },
// { invalidValue: undefined, code: 'AGE_NOT_VALID' }
// ]
explanation2,
fixedValue2 // { name: 'a' }
})
wrong types
v()
const value3 = { age: '963', name: 1 }
const test3 = isValidPerson(value3)
const explanation3 = v.explanation
const fixedValue3 = v.fix(value3)
console.log({
value3, // { age: '963', name: 1 }
test3, // false
//[
// { invalidValue: 1, code: 'NAME_IS_NOT_STRING' },
// { invalidValue: '963', code: 'AGE_NOT_VALID' }
//]
explanation3,
fixedValue3 // { name: 'b' }
})
right type, wrong values
v()
const value4 = { age: 963, name: '' }
const test4 = isValidPerson(value4)
const explanation4 = v.explanation
const fixedValue4 = v.fix(value4)
console.log({
value4, // { age: 963, name: '' }
test4, // false
//[
// { invalidValue: 1, code: 'NAME_IS_NOT_STRING' },
// { invalidValue: '963', code: 'AGE_NOT_VALID' }
//]
explanation4,
fixedValue4 //
})
Valid data
v()
const value5 = { age: 21, name: 'Maksym' }
const test5 = isValidPerson(value5)
const explanation5 = v.explanation
const fixedValue5 = v.fix(value5)
console.log({
value4, // { age: 21, name: 'Maksym' }
test4, // true
explanation4, // []
fixedValue4 // { age: 21, name: 'Maksym' }
})
Clear code
Clear code is code that you can easily understand and modify
Readable
There are some features that make the code more readable:
- object validation schema is the object with the same structure as an object that must be validated
- text aliases for validation functions
Modifiable
There are some features that make the code more modifiable:
- Easy to read sometimes means easy to modify.
- methods names and structure - makes it easier to find the place of change
- custom validation functions - allows you to make any kind of validation
Additional Possibilities
There is also several additional possibilities:
Method | Description |
---|---|
v.example(schema, ...examples) |
If examples are not valid, it will throw Error. It can be used as documentation and testing of the shema. Returns validation function, if examples are valid |
v.validOr(schema, defaultValue) |
Returns function that takes value and replace it by defaultValue if the value is not value |
v.omitInvalidProps(objectSchema) |
Returns function that takes value . If value is not an object - returns unchanged.If value is object - it tests all props that present in objectSchema and removes all props that is invalid |
v.throwError(schema, errorMessage) |
returns function that takes value .Returns value if it's valid. Throws error otherwise. Can be used in pipe of functions. |
Other Solutions
There is plenty of good validation libraries, among them ajv
, joi
, yup
, type-contract
. They are beautiful and strong. You should use them if you found that this solution - is not for you.
Contacts
Author | Andrew Beletskiy |
Position | Frontend Developer, Adraba |
AndrewBeletskiy@gmail.com | |
Github | https://github.com/whiteand |
Top comments (0)