yarn add @bytesoftio/schema
or npm install @bytesoftio/schema
- Description
- Quick start
- Testing
- Validating
- Sanitizing
- Sanitize and test
- Sanitize and validate
- Reusing validation schemas
- Relations with and() / or() / also()
- Add a custom validator
- Add a custom sanitizer
- Async methods and logic
- Alternative syntax
- Translations
- String schema
- required
- optional
- equals
- length
- min
- max
- between
- matches
- url
- startsWith
- endsWith
- includes
- omits
- oneOf
- noneOf
- numeric
- alpha
- alphaNumeric
- alphaDashes
- alphaUnderscores
- alphaNumericDashes
- alphaNumericUnderscores
- date
- dateBefore
- dateBeforeOrSame
- dateAfter
- dateAfterOrSame
- dateBetween
- dateBetweenOrSame
- time
- timeBefore
- timeBeforeOrSame
- timeAfter
- timeAfterOrSame
- timeBetween
- timeBetweenOrSame
- dateTime
- toDefault
- toUpperCase
- toLowerCase
- toCapitalized
- toCamelCase
- toSnakeCase
- toKebabCase
- toConstantCase
- toTrimmed
- Number schema
- Boolean schema
- Date schema
- Array schema
- Object schema
- required
- optional
- equals
- shape
- allowUnknownKeys
- disallowUnknownKeys
- shapeUnknownKeys
- shapeUnknownValues
- toDefault
- toCamelCaseKeys
- toCamelCaseKeysDeep
- toSnakeCaseKeys
- toSnakeCaseKeysDeep
- toKebabCaseKeys
- toKebabCaseKeysDeep
- toConstantCaseKeys
- toConstantCaseKeysDeep
- toMappedValues
- toMappedValuesDeep
- toMappedKeys
- toMappedKeysDeep
- Mixed schema
This library provides a convenient way to describe, validate and sanitize primitive values like strings and numbers, but also objects. It can be used for complex validation scenarios as well as simple one-line assertions.
There are multiple kinds of schemas for different types of data:
object
, string
, number
, array
, boolean
, date
and mixed
.
There are two ways to run assertions / validations. For simple things like
one-liners where you simply want to know if a value matches certain criteria,
with a true
/ false
as result, you can use test
. For proper validation,
with error messages, etc., you can use validate
.
Each data type specific schema comes with many different assertion and sanitization methods. Some methods are common for all of the schemas, some are available only on a certain kind of schema.
Assertions are used for validation purposes and are used to describe
the underlying value and to ensure it is valid. Methods, test
, validate
and sanitize
exist in two versions: sync and async.
Sanitization / normalization methods are used to process the underlying value even further, for example, to ensure that a string is capitalised, or all of the object keys are camel-cased, etc.
Here is an example of all the available schemas and how to import them.
import { string, number, array, boolean, date, object, mixed } from "@bytesoftio/schema"
Let's describe a simple user object.
- email must be of type string and a valid email address
- fullName must be a string between 3 and 100 characters
- roles must be an array containing at least one role, valid roles are "admin", "publisher" and "developer", not duplicates are allowed
- tags must be an array of string, at least 3 characters long, consisting of letter and dashes
import { array, object, string } from "@bytesoftio/schema"
const userSchema = object({
email: string().email(),
fullName: string().min(3).max(100),
roles: array().min(1).someOf(["admin", "publisher", "developer"]).toUnique(),
tags: array(string().min(3).alphaDashes())
})
The schema above contains some validation assertions as well as some sanitization / normalization logic.
Quick check if an object is valid according to the schema:
const valid = userSchema.test({ /* ... */ })
if (valid) {
// ...
}
Regular validation:
const errors = userSchema.validate({ /* ... */ })
if ( ! errors) {
// ...
}
Run sanitizers like array().toUnique()
:
const sanitizedValue = userSchema.sanitize({ /* ... */ })
All together:
const [valid, sanitizedValue] = userSchema.sanitizeAndTest({ /* ... */ })
const [errors, sanitizedValue] = userSchema.sanitizeAndValidate({ /* ... */ })
Lets take a look how to run simple assertions using the test
method.
Successful assertion:
import { string } from "@bytesoftio/schema"
const schema = string().min(3).alphaNumeric()
// true
const valid = schema.test("fooBar")
if (valid) {
// ...
}
Failed assertion:
import { string } from "@bytesoftio/schema"
const schema = string().min(3).alphaNumeric()
// false
const valid = schema.test("foo-bar")
if (valid) {
// ...
}
Validations can be very simple, when using strings, numbers, etc. or become quite complex when using object. We'll cover objects in a later section.
Successful validation:
import { string } from "@bytesoftio/schema"
const schema = string().min(3).alphaNumeric()
// undefined
const errors = schema.validate("fooBar")
if ( ! errors) {
// ...
}
Failed validation:
import { string } from "@bytesoftio/schema"
const schema = string().min(3).alphaNumeric()
// [ ... ]
const errors = schema.validate("foo-bar")
if ( ! errors) {
// ...
}
This is what the validation error looks like:
[
{
// identifies validation and translation key
type: 'string_alpha_numeric',
// translated validation message
message: 'Must consist of letters and digits only',
// additional arguments into the the assertion method, like string().min(1)
args: [],
// underlying value that was validated
value: 'foo-bar',
// description of logical validation links, see .or() and .and() methods
link: undefined,
// path of the validated property, when validating objects,
// using dot notation "path.to.property"
path: undefined
}
]
Lets take a look on how schema can be used to sanitize / normalize data. For convenience, all sanitization methods start with to
, like toCamelCase
.
import { string } from "@bytesoftio/schema"
const schema = string().toTrimmed().toCamelCase()
// "fooBar"
const value = schema.sanitize(" foo bar ")
Now let's mix some things up, what if you could sanitize your data before running the assertions?
Successful test:
import { string } from "@bytesoftio/schema"
const schema = string().min(4).toCamelCase()
// [true, "fooBar"]
const [valid, value] = schema.sanitizeAndTest("foo bar")
Failed test:
import { string } from "@bytesoftio/schema"
const schema = string().min(4).toTrimmed()
// [false, "foo"]
const [valid, value] = schema.sanitizeAndTest(" foo ")
As you can see, even though the string " foo "
has a length greater than 4, after it gets trimmed (all surrounding whitespace gets stripped away), it becomes"foo"
and therefore its length is less than 4.
This method works exactly the same as sanitizeAndTest
, except instead of calling test
behind the scenes, it calls the validate
method.
Successful validation:
import { string } from "@bytesoftio/schema"
const schema = string().min(4).toCamelCase()
// [undefined, "fooBar"]
const [errors, value] = schema.sanitizeAndValidate("foo bar")
Failed validation:
import { string } from "@bytesoftio/schema"
const schema = string().min(4).toTrimmed()
// [[ ... ], "foo"]
const [errors, value] = schema.sanitizeAndValidate(" foo ")
This what the errors would look like:
[
{
type: 'string_min',
message: 'Must be at least "4" characters long',
args: [ 4 ],
value: 'foo',
link: undefined,
path: undefined
}
]
Schemas can be chained using conditions. It is also possible to shape the contents of an array or object using a dedicated schema. Sounds complicated, but it isn't. Based on the reasons above you might want to split schemas into small reusable pieces.
import { array, string } from "@bytesoftio/schema"
// a valid username is alpha numeric and has a length from 3 to 10 characters
const usernameSchema = string().alphaNumeric().between(3, 10)
// array contain at least 3 valid usernames
const usernameListSchema = array().min(3).shape(usernameSchema)
// undefined
const errors = usernameListSchema.validate(["foo", "bar", "baz"])
Schemas can logically be linked together using and
and or
methods. An and
schema
will only be executed if the higher order schema, that it is linked to, could validate successfully.
An or
schema will only execute if the parent schema failed, the or
schema will be tried instead.
string().min(3).and(string().noneOf(["foo", "bar"]))
number().or(string().numeric())
Conditional schemas can also be wrapped into a callback that will be executed at validation time.
string().min(3).and(() => string().noneOf(["foo", "bar"]))
number().or(() => string().numeric())
and()
, or()
are practically interchangeable with validator()
and therefore can also return an error message directly.
number().and((value) => value < 12 && "Value must be bigger than 12")
There is also a method also()
that is basically an alias for validator()
and is syntactic sugar for some use cases.
Adding custom validation behaviour is fairly easy to do.
import { string } from "@bytesoftio/schema"
const assertMinLength = (min: number) => {
return (value) => {
if (typeof value === "string" && value.length < min) {
return "Value is too short"
}
}
}
const schema = string().validator(assertMinLength(10))
// [ ... ]
const errors = schema.validate("foo bar")
This is what the errors would look like:
[
{
type: 'custom',
message: 'Value is too short',
args: [],
value: 'foo bar',
link: undefined,
path: undefined
}
]
A validator can also return another schema.
import { string } from "@bytesoftio/schema"
const schema = string().validator(() => string().min(3))
It is very easy to hook up a custom sanitizer into an existing schema.
import { string } from "@bytesoftio/schema"
const toUpperCase = (value) => {
if (typeof value === "string") {
return value.toUpperCase()
}
return value
}
const schema = string().sanitizer(toUpperCase)
// "FOO BAR"
const value = schema.sanitize("foo bar")
Every validation, sanitizing and testing method has an async counterpart. Synchronous methods would be the go to ones, most of the time. This library itself does not come with any async validation or sanitizing logic. However, it is possible for you to add custom validation and sanitizing methods and you might need them to be async. If you try to run any kind of validation or sanitizing logic trough a sync method, you will get an error - you'll be asked to use an async mehtod instead.
const schema = object({ /* ... */ })
schema.test(/* ... */)
await schema.testAsync(/* ... */)
schema.validate(/* ... */)
await schema.validateAsync(/* ... */)
schema.sanitize(/* ... */)
await schema.sanitizeAsync(/* ... */)
schema.sanitizeAndTest(/* ... */)
await schema.sanitizeAndTestAsync(/* ... */)
schema.sanitizeAndValidate(/* ... */)
await schema.sanitizeAndValidateAsync(/* ... */)
You can create any schema starting with the default value, this is especially useful for forms.
import { value, string } from "@bytesoftio/schema"
value('').string()
// same as
string().toDefault('')
// same applies for boolean, number, date, etc. ...
This library uses @bytesoftio/translator behind the scenes. Please take a look at the corresponding docs for examples of how to add / replace translations, etc.
Access translator like this:
import { schemaTranslator } from "@bytesoftio/schema"
// take a look at available translations
schemaTranslator.getTranslations()
// customize translations
schemaTranslator.setTranslations({
en: { string_min: "Value too short" }
})
String schema has all the methods related to string validation and sanitization.
import { string } from "@bytesoftio/schema"
Value must be a non empty string. Active by default.
string().required()
// or
string().required(() => false)
Value might be a string, opposite of required
.
string().optional()
String must be equal to the given value.
string().equals("foo")
// or
string().equals(() => "foo")
String must have an exact length
string().length(3)
// or
string().length(() => 3)
String must not be shorter than given value.
string().min(3)
// or
string().min(() => 3)
String must not be longer than given value.
string().max(3)
// or
string().max(() => 3)
String must have a length between min and max.
string().between(3, 6)
// or
string().between(() => 3, () => 6)
String must match given RegExp.
string().matches(/^red/)
// or
string().matches(() => /^red/)
String must be a valid email address.
string().email()
String must be a valid URL.
string().url()
String must start with a given value.
string().startsWith("foo")
// or
string().startsWith(() => "foo")
String must end with a given value.
string().endsWith("foo")
// or
string().endsWith(() => "foo")
String must include given substring.
string().includes("foo")
// or
string().includes(() => "foo")
String must not include given substring.
string().omits("foo")
// or
string().omits(() => "foo")
String must be one of the whitelisted values.
string().oneOf(["foo", "bar"])
// or
string().oneOf(() => ["foo", "bar"])
String must not be one of the blacklisted values.
string().noneOf(["foo", "bar"])
// or
string().noneOf(() => ["foo", "bar"])
String must contain numbers only, including floats.
string().numeric()
String must contain letters only.
string().alpha()
String must contain numbers and letters only.
string().alphaNumeric()
String must contain letters and dashes "-" only.
string().alphaDashes()
String must container letters and underscores "_" only.
string().alphaUnderscores()
String must container letters, numbers and dashes only.
string().alphaNumericDashes()
String must contain letters, numbers and underscores only.
string().alphaNumericUnderscores()
String must be a valid ISO date string.
string().date()
String must be a valid ISO date string before the given date.
string().dateBefore(new Date())
// or
string().dateBefore(() => new Date())
Similar to dateBefore
, but allows dates to be equal.
string().dateBeforeOrSame(new Date())
// or
string().dateBeforeOrSame(() => new Date())
String must be a valid ISO date string after the given date.
string().dateAfter(new Date())
// or
string().dateAfter(() => new Date())
Similar to dateAfter
, but allows dates to be equal.
string().dateAfterOrSame(new Date())
// or
string().dateAfterOrSame(() => new Date())
String must be a valid ISO date string between the two given dates.
string().dateBetween(new Date(), new Date())
// or
string().dateBetween(() => new Date(), new Date())
Similar to dateBetween
, but allows dates to be equal.
string().dateBetweenOrSame(new Date(), new Date())
// or
string().dateBetweenOrSame(() => new Date(), new Date())
String must be a valid ISO time string.
string().time()
String must be a valid ISO time string before the given time.
string().timeBefore("10:00")
// or
string().timeBefore(() => "10:00")
Similar to timeBefore
, but allows times to be equal.
string().timeBeforeOrSame("10:00")
// or
string().timeBeforeOrSame(() => "10:00")
String must be a valid ISO time string after the given time.
string().timeAfter("10:00")
// or
string().timeAfter(() => "10:00")
Similar to timeAfter
, but allows times to be equal.
string().timeAfterOrSame("10:00")
// or
string().timeAfterOrSame(() => "10:00")
String must be a valid ISO time string between the two given times.
string().timeBetween("10:00", "15:00")
// or
string().timeBetween(() => "10:00", "15:00")
Similar to dateBetween
, but allows dates to be equal.
string().dateBetweenOrSame(new Date(), new Date())
// or
string().dateBetweenOrSame(() => new Date(), new Date())
String must be a valid ISO date time string.
string().dateTime()
Provide a fallback value in case the underlying value is not a string.
string().toDefault("default value")
// or
string().dateBefore(() => "default value")
Convert string to all upper case.
string().toUpperCase()
Convert string to all lower case.
string().toLowerCase()
Capitalize first letter.
string().toCapitalized()
Convert string to camelCase.
string().toCamelCase()
Convert string to snake_case.
string().toSnakeCase()
Convert string to kebab-case.
string().toKebabCase()
Convert string to CONSTANT_CASE.
string().toConstantCase()
Trim surrounding white space.
string().toTrimmed()
Number schema has all the methods related to number validation and sanitization.
import { number } from "@bytesoftio/schema"
Value must be a number.
number().required()
// or
number().required(() => false)
Value might be a number, opposite of required
.
number().optional()
Number must be equal to the given value.
number().equals(3)
// or
number().equals(() => 3)
Number must not be smaller than the given value.
number().min(5)
// or
number().min(() => 5)
Number must not be bigger than the given value.
number().max(10)
// or
number().max(() => 10)
Number must be between the two given numbers.
number().between(5, 10)
// or
number().between(() => 5, () => 10)
Number must be positive - bigger than 0.
number().positive()
Number must be negative - smaller than 0.
number().negative()
Number must be an integer - no floats.
number().integer()
Default value in case the underlying value is not a number.
number().toDefault(10)
// or
number().toDefault(() => 10)
Round value using Math.round()
.
number().toRounded(2)
// or
number().toRounded(() => 2)
Round value using Math.floor()
.
number().toFloored()
Round value using Math.ceil()
.
number().toCeiled()
Trunc value - drop everything after the decimal point.
number().toTrunced()
Boolean schema has all the methods related to boolean validation and sanitization.
import { boolean } from "@bytesoftio/schema"
Value must be a boolean.
boolean().required()
// or
boolean().required(() => false)
Value might be a boolean, opposite of required
.
boolean().optional()
Number must be equal to the given value.
boolean().equals(true)
// or
boolean().equals(() => true)
Provide a fallback value in case the underlying value is not a boolean.
boolean().toDefault(true)
// or
boolean().toDefault(() => true)
Date schema has all the methods related to date validation and sanitization.
import { date } from "@bytesoftio/schema"
Value must be a date.
date().required()
// or
date().required(() => false)
Value might be a date, opposite of required
.
date().optional()
Date must be equal to the given value.
date().equals(new Date())
// or
date().equals(() => new Date())
Underlying value must be after the given date.
date().after(new Date())
// or
date().after(() => new Date())
Underlying value must be before the given date.
date().before(new Date())
// or
date().before(() => new Date())
Underlying value must be between the two dates.
date().between(new Date(), new Date())
// or
date().between(() => new Date(), () => new Date())
Provide a fallback value in case the underlying value is not a date.
date().toDefault(new Date())
// or
date().toDefault(() => new Date())
Array schema has all the methods related to array validation and sanitization.
import { array } from "@bytesoftio/schema"
Value must be a array.
array().required()
// or
array().required(() => false)
Value might be a array, opposite of required
.
array().optional()
Array must be equal to the given value.
array().equals([1, 2])
// or
array().equals(() => [1, 2])
Array must have an exact length.
array().length(3)
// or
array().length(() => 3)
Array must not be shorter than the given length.
array().min(3)
// or
array().min(() => 3)
Array must not be longer than the given length.
array().max(3)
// or
array().max(() => 3)
Array must have a length between the two given values.
array().between(3, 5)
// or
array().between(() => 3, () => 5)
Array must only contain whitelisted values.
array().someOf([3, 4])
// or
array().someOf(() => [3, 4])
Array must not contain any of the blacklisted values.
array().noneOf([3, 4])
// or
array().noneOf(() => [3, 4])
Specify a schema for array items. Every item must be valid according to the schema.
array().shape(string().min(3))
// or
array().shape(() => string().min(3))
Provide a default value in case the underlying value is not an array.
array().toDefault([1, 2])
// or
array().toDefault(() => [1, 2])
Filter out invalid array items manually.
const isString = (value) => typeof value === "string"
array().toFiltered(isString)
Map every array item manually.
const toUpperCase = (value) => typeof value === "string" ? value.toUpperCase() : value
array().toMapped(toUpperCase)
Filter out all falsey
values like null
, undefined
, """
and 0
.
array().toCompact()
Filter out all duplicate values.
array().toUnique()
Object schema has all the methods related to object validation and sanitization.
import { object } from "@bytesoftio/schema"
Value must be a object.
object().required()
// or
object().required(() => false)
Value might be a object, opposite of required
.
object().optional()
Underlying value must be equal to the given value.
object().equals({foo: "bar"})
// or
object().equals(() => ({foo: "bar"}))
Shape an object and set up schemas for all of its properties.
object().shape({ firstName: string().min(3).max(20) })
Allow object to contain keys that have not been configured through .shape()
.
object()
.shape({ firstName: string().min(3).max(20) })
.allowUnknownKeys()
Forbid object to contain keys that have not been configured through .shape()
, active by default.
object()
.shape({ firstName: string().min(3).max(20) })
.disallowUnknownKeys()
Shape unknown object keys to make sure they adhere to a certain format / are valid.
object()
.shape({ firstName: string().min(3).max(20) })
.shapeUnknownKeys(string().min(3).toCamelCase())
Shape unknown object values to make sure they adhere to a format / are valid.
object()
.shape({ firstName: string().min(3).max(20) })
.shapeUnknownValues(string().min(3).max(20))
Provide a fallback value in case the underlying value is not an object.
object().toDefault({title: "Foo"})
// or
object().toDefault(() => ({title: "Foo"}))
Transform all object keys to camelCase.
object().toCamelCaseKeys()
Transform all object keys deeply to camelCase.
object().toCamelCaseKeysDeep()
Transform all object keys to snake_case.
object().toSnakeCaseKeys()
Transform all object keys deeply to snake_case.
object().toSnakeCaseKeysDeep()
Transform all object keys to kebab-case.
object().toKebabCaseKeys()
Transform all object keys deeply to kebab-case.
object().toKebabCaseKeysDeep()
Transform all object keys to CONSTANT_CASE.
object().toConstantCaseKeys()
Transform all object keys deeply to CONSTANT_CASE.
object().toConstantCaseKeysDeep()
Transform all object values.
object().toMappedValues((value, key) => value)
Transform all object values deeply.
object().toMappedValuesDeep((value, key) => value)
Transform all object keys.
object().toMappedKeys((value, key) => key)
Transform all object keys deeply.
object().toMappedKeysDeep((value, key) => key)
Mixed schema is used when validating / sanitizing data that can have different / unknown types.
import { mixed } from "@bytesoftio/schema"
Value must not be null
nor undefined
.
mixed().required()
// or
mixed().required(() => false)
Value might als be a null
or undefined
, opposite of required
.
mixed().optional()
Underlying value must be equal to the given value.
mixed().equals("yolo")
// or
mixed().equals(() => "yolo")
Underlying value must be one of the whitelisted values.
mixed().oneOf(["foo", "bar"])
// or
mixed().oneOf(() => ["foo", "bar"])
Underlying value must not be one of the blacklisted values.
mixed().noneOf(["foo", "bar"])
// or
mixed().noneOf(() => ["foo", "bar"])
Provide a fallback value in case the underlying value is a null
or undefined
.
mixed().toDefault(true)
// or
mixed().toDefault(() => true)