adv-parser
Parser for special simplified syntax of json schema.
Default json schema
schema = {
type: "object",
additionalProperties: false,
required: ['id', '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
const parser = require('adv-parser');
let schema = parser(`{id: number}`);
schema = parser(() => ({id: number}));
schema == {
type: 'object',
additionalProperties: false,
required: ['id'],
properties: {
id: {type: 'number'}
}
};
Schemas cache
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;
Custom methods
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,
};
Custom functions
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
}
};
Custom object inline options
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'}
}
};
Optional object fields
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"},
},
}
Array syntax
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'}]},
}
Number patterns
Instead of short number
validator you can use one of following number patterns as value of object field.
int
number without floating-pointpositive
positive number including 0
negative
negative number excluding 0
id
integer more than 0
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",
}
},
},
}
String patterns
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 requireduri
full URI.uri-reference
URI reference, including full and relative URIs.uri-template
URI template according to RFC6570email
email address.hostname
host name according to RFC1034.filename
name (words with dashes) with extensionipv4
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",
}
},
}
}
Inject external schema
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'},
}
}
}
}
anyOf schema
Instead of anyOf
you can use ||
operator
schema = {
data: User || Account || {type: "object"}
}
schema == {
type: "object",
additionalProperties: false,
required: ['data'],
properties: {
data: {
anyOf: [
{},
{},
{type: "object"},
]
}
}
}
allOf schema
Instead of allOf
you can use &&
operator
schema = {
data: User && Account && {type: "object"}
}
schema == {
type: "object",
additionalProperties: false,
required: ['data'],
properties: {
data: {
allOf: [
{},
{},
{type: "object"},
]
}
}
}
Extend schema
To extend you can use object spread operator
User = {
id: number,
data: string,
}
UserExtra = {
name: string,
created_at: date,
}
schema = {
...User,
...UserExtra,
age: number,
data: undefined,
created_at: date-time,
}
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,
}
Switch syntax
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.
Pure syntax
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,
}
Schema methods
Another great way to extend a schema is to use it methods.
Example schema
User = {
id: number,
[name]: string,
}
prop
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
}
props
Alias: pick
Returns "object" schema of props
schema = User.props('id', {name: 'full_name'})
schema == {
id: number,
[full_name]: string,
}
merge
Aliases: add
, assign
, extend
Returns extended schema
schema = User.merge({token: uuid})
schema == {
id: number,
[name]: string,
token: uuid,
}
remove
Alias: omit
Returns schema without props
schema = User.remove('id')
schema == {
[name]: string
}
required
Returns same schema, only with required props. Can take many props names.
schema = User.required('name')
schema == {
id: number,
name: string,
}
notRequired
Alias: optional
Make fields optional
schema = User.notRequired('id')
schema == {
[id]: number,
[name]: string,
}
set
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,
}
}
get
Return schema option value like minLength
schema = {
search: string.set('minLength', User.prop('name').get('minLength'))
}
Schema options as methods
All schemas options are duplicated as methods
schema = {
id: number.minimum(1),
search: string.minLength(3).maxLength(20),
}
schema = User.additionalProperties(true).maxProperties(10)
Object schema inline options
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'}
}
}