Parser for special simplified syntax of json schema.
Default json schema
schema = {
type: "object",
additionalProperties: false,
required: ['id', /* "name", */ 'enabled', 'list', 'user', 'enumOfStrings'],
properties: {
id: {
type: "number",
},
name: {
type: "string",
},
enabled: {
type: "boolean",
},
list: {
type: "array",
items: {type: "number"}
},
user: {
type: "object",
additionalProperties: false,
required: ['id', 'type'],
properties: {
id: {type: 'number'},
type: {type: 'string'},
}
},
enumOfStrings: {
type: "string",
enum: ["user", "guest", "owner"]
},
}
}
Simplified syntax of same schema
schema = {
id: number,
[name]: string,
enabled: boolean,
list: [number],
user: {
id: number,
type: string,
},
enumOfStrings: "user" || "guest" || "owner",
}
- Usage
- Optional object fields
- Array syntax
- Number patterns
- String patterns
- Inject external schema
- anyOf schema
- allOf schema
- Extend schema
- Switch syntax
- Pure syntax
- Schema methods
- Schema options as methods
- Object schema inline options
const parser = require('adv-parser');
let schema = parser(`{id: number}`);
// or as arrow function (which will be converted to string and parsed)
// if you want free syntax highlighting
schema = parser(() => ({id: number}));
schema == {
type: 'object',
additionalProperties: false,
required: ['id'],
properties: {
id: {type: 'number'}
}
};
const parser = require('adv-parser');
const defaultSchemas = require('adv-parser/schemas');
const schemas = {
...defaultSchemas
};
const schema1 = parser(`User = {id: number}`, {schemas});
const schema2 = parser(`Test.SubTest = {name: string}`, {schemas});
const schema3 = parser(Product => ({id: uuid}), {schemas});
const schema4 = parser(() => Company = {name: /^\w+$/}, {schemas});
schema1 == schemas.User;
schema2 == schemas['Test.SubTest'];
schema3 == schemas.Product;
schema4 == schemas.Company;
All methods work with the schema as AST. It gives you ability to create your own meta programming language
More about default methods see Schema methods and Schema options as methods
const parser = require('adv-parser');
const defaultMethods = require('adv-parser/methods');
const {set} = defaultMethods;
const schema = parser(`number.test(true)`, {
methods: {
...defaultMethods,
test: function (schema, args, params) {
return set(schema, ['test', args[0]], params);
}
}
});
schema == {
type: 'number',
test: true,
};
You can define custom functions like
const parser = require('adv-parser');
const t = require('@babel/types');
const schema = parser(`{id: test(1, 2)}`, {
functions: {
test: function (args) {
return t.numericLiteral(
args.reduce((sum, item) => sum + item.value, 0)
);
}
}
});
schema == {
type: 'object',
test: true,
additionalProperties: false,
required: ['id'],
properties: {
id: 3
}
};
More about default object inline options see Object schema inline options
const parser = require('adv-parser');
const defaultObjectOptions = require('adv-parser/methods/object');
const set = require('adv-parser/methods/set');
const schema = parser(`{id: number, $test: true}`, {
objectOptions: {
...defaultObjectOptions,
test: function (schema, args, params) {
return set(schema, ['test', args[0]], params);
}
}
});
schema == {
type: 'object',
test: true,
additionalProperties: false,
required: ['id'],
properties: {
id: {type: 'number'}
}
};
By default, all fields in an object are required. To make field optional just put it in brackets.
schema = {
id: number,
[name]: string,
}
schema == {
type: "object",
additionalProperties: false,
required: ["id"],
properties: {
id: {type: "number"},
name: {type: "string"},
},
}
Here example of array where all items should be validated with one schema
schema = [number]
schema == {
type: 'array',
items: {type: 'number'}
}
Here example how we can validate items through many schemas
schema = [number || string || {id: number}]
schema == {
type: 'array',
items: {
anyOf: [
{type: 'number'},
{type: 'string'},
{
type: 'object',
additionalProperties: false,
required: ['id'],
properties: {
id: {type: 'number'}
}
},
]
}
}
Here index relative validation
schema = [number, string]
Which means that first element must be a number and second a string. Rest elements validation depends on array options like additionalItems
.
In this example valid will be: [1]
, [1, "abc"]
, [1, "abc", 2]
, []
. Not valid: ["abc", 1]
, ["abc"]
schema == {
type: 'array',
items: [
{type: 'number'},
{type: 'string'}
]
}
You can add any array option with it methods
schema = [number, string].additionalItems(false)
schema == {
type: 'array',
items: [
{type: 'number'},
{type: 'string'}
],
additionalItems: false,
}
If you need one index relative element validation than you can use items
method like
firstNumber = [].items([number])
firstString = array.items([string])
firstNumber == {
type: 'array',
items: [{type: 'number'}]
}
firstString == {
type: 'array',
items: [{type: 'string'}]
}
This example means that at least one element in an array must be valid
list = [...string]
listOr = [...(string || boolean)]
list == {
type: 'array',
contains: {type: 'string'},
}
listOr == {
type: 'array',
contains: {anyOf: [{type: 'string'}, {type: 'boolean'}]},
}
Combination of index relative validation and contains
schema = [number, ...(string || boolean)]
schema == {
type: 'array',
items: [
{type: 'number'}
],
contains: {anyOf: [{type: 'string'}, {type: 'boolean'}]},
}
Instead of short number
validator you can use one of following number patterns as value of object field.
-
int
number without floating-point -
positive
positive number including0
-
negative
negative number excluding0
-
id
integer more than0
schema = {
id: id,
price: positive,
list: [int],
}
schema == {
type: "object",
additionalProperties: false,
required: ['id', 'price', 'list'],
properties: {
id: {
type: "integer",
minimum: 1,
},
price: {
type: "number",
minimum: 0,
},
list: {
type: "array",
items: {
type: "integer",
}
},
},
}
Instead of short string
validator you can use one of following string patterns as value of object field.
-
date
full-date according to RFC3339. -
time
time with optional time-zone. -
date-time
date-time from the same source (time-zone is optional, in ajv it's mandatory) -
date-time-tz
date-time with time-zone required -
uri
full URI. -
uri-reference
URI reference, including full and relative URIs. -
uri-template
URI template according to RFC6570 -
email
email address. -
hostname
host name according to RFC1034. -
filename
name (words with dashes) with extension -
ipv4
IP address v4. -
ipv6
IP address v6. -
regex
tests whether a string is a valid regular expression by passing it to RegExp constructor. -
uuid
Universally Unique Identifier according to RFC4122.
Also, regexp will be converted to {pattern: "regexp"}
schema = {
id: uuid,
email: email,
created_at: date-time,
phone: /^\+?\d+$/,
days: [date],
}
schema == {
type: "object",
additionalProperties: false,
required: ['id', 'email', 'created_at', 'phone', 'days'],
properties: {
id: {
type: "string",
format: "uuid",
},
email: {
type: "string",
format: "email",
},
created_at: {
type: "string",
format: "date-time",
},
phone: {
type: "string",
pattern: "^\\+?\\d+$",
},
days: {
type: "array",
items: {
type: "string",
format: "date",
}
},
}
}
You can inject an external schema in a current schema.
User = {
id: number,
name: string,
}
schema = {
action: 'update' || 'delete',
user: User,
}
schema == {
type: 'object',
additionalProperties: false,
required: ['action', 'user'],
properties: {
action: {
type: 'string',
enum: ['update', 'delete']
},
user: {
type: 'object',
additionalProperties: false,
required: ['id', 'name'],
properties: {
id: {type: 'number'},
name: {type: 'string'},
}
}
}
}
Instead of anyOf
you can use ||
operator
schema = {
data: User || Account || {type: "object"}
}
schema == {
type: "object",
additionalProperties: false,
required: ['data'],
properties: {
data: {
anyOf: [
{/* schema of User */},
{/* schema of Account */},
{type: "object"},
]
}
}
}
Instead of allOf
you can use &&
operator
schema = {
data: User && Account && {type: "object"}
}
schema == {
type: "object",
additionalProperties: false,
required: ['data'],
properties: {
data: {
allOf: [
{/* schema of User */},
{/* schema of Account */},
{type: "object"},
]
}
}
}
To extend you can use object spread operator
User = {
id: number,
data: string,
}
UserExtra = {
name: string,
created_at: date,
}
schema = {
...User,
...UserExtra,
age: number, // add field
data: undefined, // remove field
created_at: date-time, // overwrite field
}
schema == {
type: "object",
additionalProperties: false,
required: ['id', 'name', 'created_at', 'age'],
properties: {
id: {type: "number"},
name: {type: "string"},
created_at: {type: "string", format: "date-time"},
age: {type: "number"},
}
}
Also, you can overwrite validator options
schema = {
...User,
type: "object",
additionalProperties: true,
}
Important to add type: "object"
it says to compiler that this object is pure ajv validator, not simplified version.
schema = {
type: "object",
additionalProperties: true,
properties: {
id: {type: "number"},
data: {type: "string"},
}
}
You extend even non object validators
phone = {
type: "string",
pattern: "^\\d+$"
}
schema = {
...phone,
type: "string",
maxLength: 20,
}
This syntax useful in case when you write something like {...} || {...} || {...}
but if validator found error then it throws tons of messages from each of that object.
To help validator to figure out which object is responsible for current data you can use next syntax
schema = (
(
{action: 'create'} >>
{
name: string
}
)
||
(
{action: 'update'} >>
{
id: int,
name: string,
}
)
||
(
{action: 'delete'} >>
{
id: int
}
)
)
It will be converted to
schema = {
if: {
type: 'object',
additionalProperties: true,
required: ['action'],
properties: {
action: {const: 'create'}
}
},
then: {
type: 'object',
additionalProperties: false,
required: ['action', 'name'],
properties: {
action: {const: 'create'},
name: {type: 'string'},
}
},
else: {
if: {
type: 'object',
additionalProperties: true,
required: ['action'],
properties: {
action: {const: 'update'}
}
},
then: {
type: 'object',
additionalProperties: false,
required: ['action', 'id', 'name'],
properties: {
action: {const: 'update'},
id: {type: 'integer'},
name: {type: 'string'},
}
},
else: {
if: {
type: 'object',
additionalProperties: true,
required: ['action'],
properties: {
action: {const: 'delete'}
}
},
then: {
type: 'object',
additionalProperties: false,
required: ['action', 'id'],
properties: {
action: {const: 'delete'},
id: {type: 'integer'},
}
},
else: {
oneOf: [
{
type: 'object',
additionalProperties: true,
required: ['action'],
properties: {
action: {const: 'create'}
}
},
{
type: 'object',
additionalProperties: true,
required: ['action'],
properties: {
action: {const: 'update'}
}
},
{
type: 'object',
additionalProperties: true,
required: ['action'],
properties: {
action: {const: 'delete'}
}
},
]
}
}
}
}
Notice additionalProperties: true
in each if:
it means we are validating only part of object and additionalProperties: false
in then:
with same properties from if:
which means we are validating hole object.
Also we need last else: {oneOf: [...]}
to throw error that none of if:
is not matched.
If you want to write pure json schema than use !!
operator (all included schemas will be converted).
schema = {
id: !!{type: 'number'},
data: !!{
type: 'object',
properties: {
name: string.minLength(1),
},
},
}
schema = {
type: 'object',
additionalProperties: false,
required: ['id', 'data'],
properties: {
id: {type: 'number'},
data: {
type: 'object',
properties: {
name: {
type: 'string',
minLength: 1,
},
},
},
},
}
You can mix pure syntax inside regular syntax
schema = {
id: int,
...!!{
additionalProperties: true,
someOtherOption: true,
},
}
schema = {
type: 'object',
additionalProperties: true,
required: ['id'],
properties: {
id: {type: 'integer'}
},
someOtherOption: true,
}
Another great way to extend a schema is to use it methods.
Example schema
User = {
id: number,
[name]: string,
}
Returns schema of property.
Here good way to reuse schema props, even if they super simple like number
schema = {
id: User.prop('id')
}
schema == {
id: number
}
Alias: pick
Returns "object" schema of props
schema = User.props('id', {name: 'full_name'})
schema == {
id: number,
[full_name]: string,
}
Aliases: add
, assign
, extend
Returns extended schema
schema = User.merge({token: uuid})
schema == {
id: number,
[name]: string,
token: uuid,
}
Alias: omit
Returns schema without props
schema = User.remove('id')
schema == {
[name]: string
}
Returns same schema, only with required props. Can take many props names.
schema = User.required('name')
schema == {
id: number,
name: string,
}
Alias: optional
Make fields optional
schema = User.notRequired('id')
schema == {
[id]: number,
[name]: string,
}
Set schema option like additionalProperties
or minLength
schema = User.set('additionalProperties', true)
schema == {
type: "object",
additionalProperties: true,
required: ['id'],
properties: {
id: {type: "number"},
name: {type: "string"},
},
}
schema = {search: string.set('minLength', 3)}
schema == {
search: {
type: "string",
minLength: 3,
}
}
Return schema option value like minLength
schema = {
search: string.set('minLength', User.prop('name').get('minLength'))
}
All schemas options are duplicated as methods
schema = {
id: number.minimum(1),
search: string.minLength(3).maxLength(20),
}
schema = User.additionalProperties(true).maxProperties(10)
All object options can be specified inline with properties with $
sign at the beginning of option name
schema = {
id: number,
$additionalProperties: true,
$maxProperties: 10,
}
schema == {
type: 'object',
additionalProperties: true,
maxProperties: 10,
required: ['id'],
properties: {
id: {type: 'number'}
}
}