@bytesoftio/schema
v3.7.0
Published
## Installation
Downloads
38
Readme
@bytesoftio/schema
Installation
yarn add @bytesoftio/schema
or npm install @bytesoftio/schema
Table of contents
- 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
Description
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.
Quick start
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({ /* ... */ })
Testing
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) {
// ...
}
Validating
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
}
]
Sanitizing
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 ")
Sanitize and test
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.
Sanitize and validate
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
}
]
Reusing validation schemas
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"])
Relations with and() / or() / also()
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.
Add a custom validator
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))
Add a custom sanitizer
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")
Async methods and logic
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(/* ... */)
Alternative syntax
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. ...
Translations
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
String schema has all the methods related to string validation and sanitization.
import { string } from "@bytesoftio/schema"
required
Value must be a non empty string. Active by default.
string().required()
// or
string().required(() => false)
optional
Value might be a string, opposite of required
.
string().optional()
equals
String must be equal to the given value.
string().equals("foo")
// or
string().equals(() => "foo")
length
String must have an exact length
string().length(3)
// or
string().length(() => 3)
min
String must not be shorter than given value.
string().min(3)
// or
string().min(() => 3)
max
String must not be longer than given value.
string().max(3)
// or
string().max(() => 3)
between
String must have a length between min and max.
string().between(3, 6)
// or
string().between(() => 3, () => 6)
matches
String must match given RegExp.
string().matches(/^red/)
// or
string().matches(() => /^red/)
String must be a valid email address.
string().email()
url
String must be a valid URL.
string().url()
startsWith
String must start with a given value.
string().startsWith("foo")
// or
string().startsWith(() => "foo")
endsWith
String must end with a given value.
string().endsWith("foo")
// or
string().endsWith(() => "foo")
includes
String must include given substring.
string().includes("foo")
// or
string().includes(() => "foo")
omits
String must not include given substring.
string().omits("foo")
// or
string().omits(() => "foo")
oneOf
String must be one of the whitelisted values.
string().oneOf(["foo", "bar"])
// or
string().oneOf(() => ["foo", "bar"])
noneOf
String must not be one of the blacklisted values.
string().noneOf(["foo", "bar"])
// or
string().noneOf(() => ["foo", "bar"])
numeric
String must contain numbers only, including floats.
string().numeric()
alpha
String must contain letters only.
string().alpha()
alphaNumeric
String must contain numbers and letters only.
string().alphaNumeric()
alphaDashes
String must contain letters and dashes "-" only.
string().alphaDashes()
alphaUnderscores
String must container letters and underscores "_" only.
string().alphaUnderscores()
alphaNumericDashes
String must container letters, numbers and dashes only.
string().alphaNumericDashes()
alphaNumericUnderscores
String must contain letters, numbers and underscores only.
string().alphaNumericUnderscores()
date
String must be a valid ISO date string.
string().date()
dateBefore
String must be a valid ISO date string before the given date.
string().dateBefore(new Date())
// or
string().dateBefore(() => new Date())
dateBeforeOrSame
Similar to dateBefore
, but allows dates to be equal.
string().dateBeforeOrSame(new Date())
// or
string().dateBeforeOrSame(() => new Date())
dateAfter
String must be a valid ISO date string after the given date.
string().dateAfter(new Date())
// or
string().dateAfter(() => new Date())
dateAfterOrSame
Similar to dateAfter
, but allows dates to be equal.
string().dateAfterOrSame(new Date())
// or
string().dateAfterOrSame(() => new Date())
dateBetween
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())
dateBetweenOrSame
Similar to dateBetween
, but allows dates to be equal.
string().dateBetweenOrSame(new Date(), new Date())
// or
string().dateBetweenOrSame(() => new Date(), new Date())
time
String must be a valid ISO time string.
string().time()
timeBefore
String must be a valid ISO time string before the given time.
string().timeBefore("10:00")
// or
string().timeBefore(() => "10:00")
timeBeforeOrSame
Similar to timeBefore
, but allows times to be equal.
string().timeBeforeOrSame("10:00")
// or
string().timeBeforeOrSame(() => "10:00")
timeAfter
String must be a valid ISO time string after the given time.
string().timeAfter("10:00")
// or
string().timeAfter(() => "10:00")
timeAfterOrSame
Similar to timeAfter
, but allows times to be equal.
string().timeAfterOrSame("10:00")
// or
string().timeAfterOrSame(() => "10:00")
timeBetween
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")
timeBetweenOrSame
Similar to dateBetween
, but allows dates to be equal.
string().dateBetweenOrSame(new Date(), new Date())
// or
string().dateBetweenOrSame(() => new Date(), new Date())
dateTime
String must be a valid ISO date time string.
string().dateTime()
toDefault
Provide a fallback value in case the underlying value is not a string.
string().toDefault("default value")
// or
string().dateBefore(() => "default value")
toUpperCase
Convert string to all upper case.
string().toUpperCase()
toLowerCase
Convert string to all lower case.
string().toLowerCase()
toCapitalized
Capitalize first letter.
string().toCapitalized()
toCamelCase
Convert string to camelCase.
string().toCamelCase()
toSnakeCase
Convert string to snake_case.
string().toSnakeCase()
toKebabCase
Convert string to kebab-case.
string().toKebabCase()
toConstantCase
Convert string to CONSTANT_CASE.
string().toConstantCase()
toTrimmed
Trim surrounding white space.
string().toTrimmed()
Number schema
Number schema has all the methods related to number validation and sanitization.
import { number } from "@bytesoftio/schema"
required
Value must be a number.
number().required()
// or
number().required(() => false)
optional
Value might be a number, opposite of required
.
number().optional()
equals
Number must be equal to the given value.
number().equals(3)
// or
number().equals(() => 3)
min
Number must not be smaller than the given value.
number().min(5)
// or
number().min(() => 5)
max
Number must not be bigger than the given value.
number().max(10)
// or
number().max(() => 10)
between
Number must be between the two given numbers.
number().between(5, 10)
// or
number().between(() => 5, () => 10)
between
Number must be positive - bigger than 0.
number().positive()
negative
Number must be negative - smaller than 0.
number().negative()
integer
Number must be an integer - no floats.
number().integer()
toDefault
Default value in case the underlying value is not a number.
number().toDefault(10)
// or
number().toDefault(() => 10)
toRounded
Round value using Math.round()
.
number().toRounded(2)
// or
number().toRounded(() => 2)
toFloored
Round value using Math.floor()
.
number().toFloored()
toCeiled
Round value using Math.ceil()
.
number().toCeiled()
toTrunced
Trunc value - drop everything after the decimal point.
number().toTrunced()
Boolean schema
Boolean schema has all the methods related to boolean validation and sanitization.
import { boolean } from "@bytesoftio/schema"
required
Value must be a boolean.
boolean().required()
// or
boolean().required(() => false)
optional
Value might be a boolean, opposite of required
.
boolean().optional()
equals
Number must be equal to the given value.
boolean().equals(true)
// or
boolean().equals(() => true)
toDefault
Provide a fallback value in case the underlying value is not a boolean.
boolean().toDefault(true)
// or
boolean().toDefault(() => true)
Date schema
Date schema has all the methods related to date validation and sanitization.
import { date } from "@bytesoftio/schema"
required
Value must be a date.
date().required()
// or
date().required(() => false)
optional
Value might be a date, opposite of required
.
date().optional()
equals
Date must be equal to the given value.
date().equals(new Date())
// or
date().equals(() => new Date())
after
Underlying value must be after the given date.
date().after(new Date())
// or
date().after(() => new Date())
before
Underlying value must be before the given date.
date().before(new Date())
// or
date().before(() => new Date())
between
Underlying value must be between the two dates.
date().between(new Date(), new Date())
// or
date().between(() => new Date(), () => new Date())
toDefault
Provide a fallback value in case the underlying value is not a date.
date().toDefault(new Date())
// or
date().toDefault(() => new Date())
Array schema
Array schema has all the methods related to array validation and sanitization.
import { array } from "@bytesoftio/schema"
required
Value must be a array.
array().required()
// or
array().required(() => false)
optional
Value might be a array, opposite of required
.
array().optional()
equals
Array must be equal to the given value.
array().equals([1, 2])
// or
array().equals(() => [1, 2])
length
Array must have an exact length.
array().length(3)
// or
array().length(() => 3)
min
Array must not be shorter than the given length.
array().min(3)
// or
array().min(() => 3)
max
Array must not be longer than the given length.
array().max(3)
// or
array().max(() => 3)
between
Array must have a length between the two given values.
array().between(3, 5)
// or
array().between(() => 3, () => 5)
someOf
Array must only contain whitelisted values.
array().someOf([3, 4])
// or
array().someOf(() => [3, 4])
noneOf
Array must not contain any of the blacklisted values.
array().noneOf([3, 4])
// or
array().noneOf(() => [3, 4])
shape
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))
toDefault
Provide a default value in case the underlying value is not an array.
array().toDefault([1, 2])
// or
array().toDefault(() => [1, 2])
toFiltered
Filter out invalid array items manually.
const isString = (value) => typeof value === "string"
array().toFiltered(isString)
toMapped
Map every array item manually.
const toUpperCase = (value) => typeof value === "string" ? value.toUpperCase() : value
array().toMapped(toUpperCase)
toCompact
Filter out all falsey
values like null
, undefined
, """
and 0
.
array().toCompact()
toUnique
Filter out all duplicate values.
array().toUnique()
Object schema
Object schema has all the methods related to object validation and sanitization.
import { object } from "@bytesoftio/schema"
required
Value must be a object.
object().required()
// or
object().required(() => false)
optional
Value might be a object, opposite of required
.
object().optional()
equals
Underlying value must be equal to the given value.
object().equals({foo: "bar"})
// or
object().equals(() => ({foo: "bar"}))
shape
Shape an object and set up schemas for all of its properties.
object().shape({ firstName: string().min(3).max(20) })
allowUnknownKeys
Allow object to contain keys that have not been configured through .shape()
.
object()
.shape({ firstName: string().min(3).max(20) })
.allowUnknownKeys()
disallowUnknownKeys
Forbid object to contain keys that have not been configured through .shape()
, active by default.
object()
.shape({ firstName: string().min(3).max(20) })
.disallowUnknownKeys()
shapeUnknownKeys
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())
shapeUnknownValues
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))
toDefault
Provide a fallback value in case the underlying value is not an object.
object().toDefault({title: "Foo"})
// or
object().toDefault(() => ({title: "Foo"}))
toCamelCaseKeys
Transform all object keys to camelCase.
object().toCamelCaseKeys()
toCamelCaseKeysDeep
Transform all object keys deeply to camelCase.
object().toCamelCaseKeysDeep()
toSnakeCaseKeys
Transform all object keys to snake_case.
object().toSnakeCaseKeys()
toSnakeCaseKeysDeep
Transform all object keys deeply to snake_case.
object().toSnakeCaseKeysDeep()
toKebabCaseKeys
Transform all object keys to kebab-case.
object().toKebabCaseKeys()
toKebabCaseKeysDeep
Transform all object keys deeply to kebab-case.
object().toKebabCaseKeysDeep()
toConstantCaseKeys
Transform all object keys to CONSTANT_CASE.
object().toConstantCaseKeys()
toConstantCaseKeysDeep
Transform all object keys deeply to CONSTANT_CASE.
object().toConstantCaseKeysDeep()
toMappedValues
Transform all object values.
object().toMappedValues((value, key) => value)
toMappedValuesDeep
Transform all object values deeply.
object().toMappedValuesDeep((value, key) => value)
toMappedKeys
Transform all object keys.
object().toMappedKeys((value, key) => key)
toMappedKeysDeep
Transform all object keys deeply.
object().toMappedKeysDeep((value, key) => key)
Mixed schema
Mixed schema is used when validating / sanitizing data that can have different / unknown types.
import { mixed } from "@bytesoftio/schema"
required
Value must not be null
nor undefined
.
mixed().required()
// or
mixed().required(() => false)
optional
Value might als be a null
or undefined
, opposite of required
.
mixed().optional()
equals
Underlying value must be equal to the given value.
mixed().equals("yolo")
// or
mixed().equals(() => "yolo")
oneOf
Underlying value must be one of the whitelisted values.
mixed().oneOf(["foo", "bar"])
// or
mixed().oneOf(() => ["foo", "bar"])
noneOf
Underlying value must not be one of the blacklisted values.
mixed().noneOf(["foo", "bar"])
// or
mixed().noneOf(() => ["foo", "bar"])
toDefault
Provide a fallback value in case the underlying value is a null
or undefined
.
mixed().toDefault(true)
// or
mixed().toDefault(() => true)