@jalik/schema
v4.2.0
Published
A utility to validate complex objects using schemas
Downloads
149
Maintainers
Readme
@jalik/schema
Features
- Define object structure and constraints
- Several constraints available
- type, required, length, min, max, minWords, maxWords, allowed, denied, pattern, format
- Custom check using a function
- Full object validation
- Object field validation
- Specific errors are thrown for each field
- Similar to the JSON Schema specification
- TypeScript declarations ♥
Sandbox
Play with the lib here: https://codesandbox.io/s/jalik-schema-demo-5o4fnk?file=/src/index.js
Installing
npm i -P @jalik/schema
yarn add @jalik/schema
Creating a schema
Let's start with the schema of a person, very simple.
import { Schema } from '@jalik/schema';
const PersonSchema = new Schema({
age: {
type: 'integer',
required: true,
min: 1,
},
gender: {
type: 'string',
required: true,
allowed: ['male', 'female'],
},
hobbies: {
type: 'array',
allowed: ['coding', 'playing', 'sleeping'],
},
name: {
type: 'string',
required: true,
maxLength: 50,
},
});
export default PersonSchema;
Extending a schema (inheritance)
To create a new schema based on another, we can extend the base schema.
import PersonSchema from './PersonSchema';
const ParentSchema = PersonSchema.extend({
children: {
type: 'array',
items: { type: PersonSchema }
},
married: {
type: 'boolean',
required: true,
},
spouse: {
type: PersonSchema
}
});
export default ParentSchema;
Cloning a schema
import { Schema } from '@jalik/schema';
const ProductSchema = new Schema({
name: {
type: 'string',
required: true
},
price: {
type: 'number',
required: true,
min: 0,
}
});
export default ProductSchema;
export const ASchema = ProductSchema.clone();
export const BSchema = ProductSchema.clone();
export const CSchema = ProductSchema.clone();
Validating objects using a schema
You want to validate an object with a schema, for sure! This is why you're here.
import { Schema } from '@jalik/schema';
const PhoneSchema = new Schema({
code: {
type: 'string',
required: true,
},
number: {
type: 'string',
required: true,
},
});
const UserSchema = new Schema({
age: {
type: 'integer',
required: true,
min: 18,
},
gender: {
type: 'string',
required: true,
allowed: ['male', 'female'],
},
name: {
type: 'string',
required: true,
maxLength: 50,
},
phone: {
type: PhoneSchema,
},
});
// A valid object
const validUser = {
age: 33,
gender: 'male',
name: 'me',
};
// An invalid object
const invalidUser = {
age: 16,
gender: null,
phone: { code: 777, number: 10101001 },
};
try {
// This will not throw an error.
UserSchema.validate(validUser);
// This will throw a ValidationError.
UserSchema.validate(invalidUser);
} catch (error) {
if (error instanceof ValidationError) {
console.log(error.errors);
}
}
// It's also possible to check if a schema is valid without throwing error
if (UserSchema.isValid(invalidUser)) {
console.error('user is not valid');
}
The ValidationError
object looks like this:
{
"reason": "object-invalid",
"message": "Object is not valid",
"errors": {
"age": "\"age\" must be greater than or equal to 16",
"gender": "\"gender\" cannot be null",
"name": "\"name\" is required",
"phone.code": "\"code\" is not of type \"string\"",
"phone.number": "\"number\" is not of type \"string\""
}
}
Note that you can get errors without having to "try/catch", by using the getErrors()
method.
If there are no errors, it will return null
;
// this example would return errors because phone.code and phone.number are not of type "string"
const errors = UserSchema.getErrors({
age: 16,
gender: null,
phone: { code: 777, number: 10101001 }
});
Handling errors
If the schema fails to validate an object, it will throw a specific error (ex: FieldLengthError
, FieldTypeError
, FieldRequiredError
...), which is helpful to handle errors display.
For example, a FieldRequiredError
looks like this:
{
"field": "Phone Number",
"message": "The field is required",
"path": "phones[0].number",
"reason": "field-required"
}
If you need to detect the type of the error, it is recommended to check the reason
attribute of
the error against a constant, instead of comparing class instance.
Here is the list of all errors types constants:
import {
ERROR_FIELD_ALLOWED,
ERROR_FIELD_DENIED,
ERROR_FIELD_FORMAT,
ERROR_FIELD_INVALID,
ERROR_FIELD_LENGTH,
ERROR_FIELD_MAX,
ERROR_FIELD_MAX_LENGTH,
ERROR_FIELD_MAX_WORDS,
ERROR_FIELD_MIN,
ERROR_FIELD_MIN_LENGTH,
ERROR_FIELD_MIN_WORDS,
ERROR_FIELD_PATTERN,
ERROR_FIELD_REQUIRED,
ERROR_FIELD_TYPE,
ERROR_FIELD_UNKNOWN,
} from '@jalik/schema';
All errors are also available as an array:
import { errors } from '@jalik/schema';
Translating errors
The first thing to do is to load the locales you need with setLocale(locale, object)
.
This can be done when your app starts or right before displaying an error.
Errors are in English by default, you can customize them or use them as this.
import { default as en } from '@jalik/schema/dist/locales/en';
import { default as fr } from '@jalik/schema/dist/locales/fr';
import { ERROR_FIELD_INVALID } from '@jalik/schema';
// Load default error messages in english.
setLocaleMessages('en', en);
// Or customize error messages.
setLocaleMessages('en', {
...en,
[ERROR_FIELD_INVALID]: 'This field is not valid :('
});
// Load other locales if needed.
setLocaleMessages('fr', fr);
Then somewhere in your application, when you want to display the error, use
the getErrorMessage(error, locale)
function.
import {
getErrorMessage,
Schema
} from '@jalik/schema';
const locale = 'fr';
// Prepare a schema.
const schema = new Schema({ age: { type: 'number' } });
// Check errors.
const errors = schema.getErrors({ age: '42' });
// Display the translated error messages.
Object.entries(errors).forEach(([path, error]) => {
const message = getErrorMessage(error, locale);
console.log(message);
});
Contributions to translations are welcome, just create a pull request with the new locale files.
Checking field's type
Use type
to check the type of the field value. It can be a basic type (array, boolean, number,
object, string), or an advanced type like an instance of Schema
or an object constructor
like Date
.
- Accepts
"array"
,"boolean"
,"integer"
,"number"
,"object"
,"string"
,Date
, or aSchema
- Throws
FieldTypeError
import { Schema } from '@jalik/schema';
export const ExampleSchema = new Schema({
// The field must be an array of any values.
array: { type: 'array' },
// The field must be a boolean.
boolean: { type: 'boolean' },
// The field must be an integer.
integer: { type: 'integer' },
// The field must be a number (integer or float).
number: { type: 'number' },
// The field must be an object.
object: { type: 'object' },
// The field must be a string.
string: { type: 'string' },
// The field must matches UserSchema.
example: { type: ExampleSchema },
// The field must be an array of objects matching UserSchema.
examples: { type: 'array', items: { type: ExampleSchema } },
// The field must be an array of arrays.
arrayArray: { type: 'array', items: { type: 'array' } },
// The field must be an array of booleans.
booleanArray: { type: 'array', items: { type: 'boolean' } },
// The field must be an array of integers.
integerArray: { type: 'array', items: { type: 'integer' } },
// The field must be an array of numbers.
numberArray: { type: 'array', items: { type: 'number' } },
// The field must be an array of objects.
objectArray: { type: 'array', items: { type: 'object' } },
// The field must be an array of strings.
stringArray: { type: 'array', items: { type: 'string' } },
});
Checking required values
Use required
to check if a field is null
or undefined
.
Important: Since v4.0.0, a FieldRequired
error will be thrown for null
values, which was not the
case in previous versions (only with undefined
)
- Accepts
Boolean
orFunction
- Throws
FieldRequiredError
import { Schema } from '@jalik/schema';
export const ExampleSchema = new Schema({
// The field is optional (this is the default).
optional: {
required: false
},
// The field must be defined.
required: {
required: true
},
});
Checking maximum and minimum values
Use max
and min
to check if a field value is below or above a limit.
- Accepts
Number
orFunction
- Throws
FieldMaxError
,FieldMinError
import { Schema } from '@jalik/schema';
export const ExampleSchema = new Schema({
// The date must be between the previous hour and the next hour.
date: {
type: Date,
min: () => new Date(Date.now() - 3600 * 1000),
max: () => new Date(Date.now() + 3600 * 1000)
},
// The number must be negative.
negativeNumber: {
type: 'number',
max: -1
},
// The number must be positive.
positiveNumber: {
type: 'number',
min: 0
}
});
Checking multiple of a number
Use multipleOf
to accept only value that is a multiple of a number.
import { Schema } from '@jalik/schema'
const schema = new Schema({
channels: {
type: 'number',
multipleOf: 2
}
})
// This is valid
schema.validate({
channels: 8
})
Checking length
Use maxLength
and minLength
to check the length of a field value. It works on any object with
a length
attribute (String
, Array
...), so if you have objects like MyList.length
, it will
work too.
- Accepts
Number
orFunction
- Throws
FieldMaxLengthError
,FieldMinLengthError
import { Schema } from '@jalik/schema';
export const ExampleSchema = new Schema({
// The array must have exactly two values.
arrayLength: {
type: 'array',
length: 2
},
// The string must have exactly ten characters.
fixedLength: {
type: 'string',
length: 10
},
// The string must have at least three characters.
minLength: {
type: 'string',
minLength: 3
},
// The array must have ten values or less.
maxLength: {
type: 'string',
maxLength: 10
},
// The string must have between five and ten characters (inclusive).
minMaxLength: {
type: 'string',
minLength: 5,
maxLength: 10
},
// It also works with objects having a length attribute.
objectWithLength: {
type: 'object',
minLength: 1,
maxLength: 1
}
});
Checking words count
Use maxWords
and minWords
to limit words count in a string.
- Accepts
Number
orFunction
- Throws
FieldMaxWordsError
,FieldMinWordsError
import { Schema } from '@jalik/schema';
export const ExampleSchema = new Schema({
// The summary must not have more than 50 words.
summary: {
type: 'string',
maxWords: 50
},
// The description must have at least ten words.
description: {
type: 'string',
minWords: 10
},
});
Checking minimum/maximum items
Use minItems
and maxItems
to limit the number of items in an array.
import { Schema } from '@jalik/schema'
const schema = new Schema({
emails: {
type: 'array',
minItems: 1,
maxItems: 2,
items: {
type: 'string'
}
}
})
// This is valid
schema.validate({
emails: [
'[email protected]',
'[email protected]',
]
})
Checking uniqueness of items
Use uniqueItems
to force having unique items in an array.
import { Schema } from '@jalik/schema'
const schema = new Schema({
emails: {
type: 'array',
uniqueItems: true,
items: {
type: 'string'
}
}
})
// This is valid
schema.validate({
emails: [
'[email protected]',
'[email protected]',
]
})
Checking allowed values
Use allowed
to check if a field value is allowed.
- Accepts
Boolean
orFunction
- Throws
FieldAllowedError
import { Schema } from '@jalik/schema';
export const ExampleSchema = new Schema({
// The string must be 'yes' or 'no'.
answer: {
type: 'string',
allowed: ['yes', 'no']
},
// The array must contain only 0 and 1 as numbers.
binaryNumbers: {
type: 'array',
items: { type: 'number' },
allowed: [0, 1]
},
// The array must contain only hot colors.
hotColors: {
type: 'array',
items: { type: 'string' },
allowed: ['red', 'yellow', 'orange']
},
});
Checking denied values
Use denied
to check if a field value is denied.
- Accepts
Boolean
orFunction
- Throws
FieldDeniedError
import { Schema } from '@jalik/schema';
export const ExampleSchema = new Schema({
message: {
type: 'string',
denied: ['duck', 'six', 'slot']
},
});
Checking using a pattern (regular expression)
Use pattern
to check if a field value matches a regular expression.
- Accepts
RegExp
,String
orFunction
- Throws
FieldPatternError
import { Schema } from '@jalik/schema';
export const ExampleSchema = new Schema({
time: {
type: 'string',
// The time must be like 'HH:mm'
pattern: '^\\d{1,2}:\\d{1,2}$'
},
password: {
type: 'string',
// The password must contain alphanumeric and special characters
pattern: /^[a-zA-Z0-9_&#@$*%?!]+$/
}
});
Checking a format (based on regular expression)
Use format
to check if a field value matches a specific known format.
- Accepts
String
orFunction
- Throws
FieldFormatError
import { Schema } from '@jalik/schema';
export const ExampleSchema = new Schema({
date: {
type: 'string',
format: 'date'
},
datetime: {
type: 'string',
format: 'date-time'
},
email: {
type: 'string',
format: 'email'
},
hostname: {
type: 'string',
format: 'hostname'
},
ipv4: {
type: 'string',
format: 'ipv4'
},
ipv6: {
type: 'string',
format: 'ipv6'
},
time: {
type: 'string',
format: 'time'
},
uri: {
type: 'string',
format: 'uri'
},
});
Checking using a function
Use check
to apply custom checks that are not possible with the schema.
- Accepts
Function
- Throws
FieldError
import { Schema } from '@jalik/schema';
export const ExampleSchema = new Schema({
evenNumber: {
type: 'number',
// The number must be even.
check: (value) => value % 2 === 0
}
});
Setting field's label
Use label
to set field's label, which will be used in errors, note that if the label is not set,
the field's name will be used instead.
- Accepts
String
orFunction
import { Schema } from '@jalik/schema';
export const ExampleSchema = new Schema({
birthday: {
type: Date,
label: 'Date of Birth'
},
});
Parsing values
Use parse
to parse string values before validation.
- Accepts
Function
import { Schema } from '@jalik/schema';
import DateTime from 'luxon';
export const ExampleSchema = new Schema({
birthday: {
type: 'string',
parse: (value) => DateTime.fromISO(value).toJSDate()
},
});
Preparing values
Use prepare
to perform some operations before validation.
- Accepts
Function
import { Schema } from '@jalik/schema';
export const ExampleSchema = new Schema({
// Execute the prepare function on field value
// before clean and check execution.
// It can be useful in some case
// where clean cannot be used to do what you want.
numbers: {
type: 'array',
items: { type: 'number' },
prepare: (numbers) => numbers.sort()
},
});
const result = ExampleSchema.prepare({ numbers: [5, 9, 0, 3, 2, 7] })
So result
will be:
{
"numbers": [
0,
2,
3,
5,
7,
9
]
}
Cleaning values
Use clean
to perform some cleaning on a value.
- Accepts
Function
import { Schema } from '@jalik/schema';
export const ExampleSchema = new Schema({
// Every items in the list will be trimmed and lowercase.
list: {
type: 'array',
items: { type: 'string' },
clean: (list) => list.map((item) => item.trim().toLowerCase())
},
});
Dynamic field properties
Almost all field properties accept a function, it is useful to return a constraint based on some conditions. The function is called with a single argument representing the current context (data) being validated by the schema.
import { Schema } from '@jalik/schema';
const isPublishing = function (context) {
// context refers to the data being validated
return context.status === 'published';
};
const PostSchema = new Schema({
title: {
type: 'string',
required: isPublishing
},
text: {
type: 'string',
required: isPublishing
},
status: {
type: 'string',
required: true,
allowed: ['published', 'draft']
}
});
// So this is valid
PostSchema.validate({
title: 'Hello World',
text: 'This is a hello world post !',
status: 'published'
});
// And this is valid too..
PostSchema.validate({
status: 'draft'
});
// But this is not valid !
PostSchema.validate({
title: 'Hello World',
text: null,
status: 'published'
});
Changelog
History of releases is in the changelog.
License
The code is released under the MIT License.