Write JSON Schema In a Graceful Way.
JSON Schema is an excellent tool for validating the structure of JSON data. However, it is also veeeeeerbose. So here comes the semantic-schema
. It let you semantically write JSON Schema so you can get rid of the verbose grammar.
This project attempt to achieve three goals:
- Use JSON Schema as an underlying data structure describer;
- Get rid of the verbose grammar of JSON Schema;
- Block some confusing feature of JSON Schema.
OK, let's begin with a compare between semantic-schema
and plain JSON Schema. If I want a data to be an object, and has 'name', 'age' and 'gender' as its properties. And there is also some limit on these properties.
Let's declare it in JSON Schema:
let schema = {
type: "object",
additionalProperties: false,
properties: {
name: {
type: "string",
pattern: "^[A-Za-z]{5}$"
},
age: {
type: "integer",
minimum: 0,
maximum: 120
},
gender: {
type: "string",
enum: ['m', 'f']
}
},
required: ['name', 'age', 'gender']
}
In semantic-schema
, we declare it like this:
const {integer} = require('semantic-schema').schema;
let schema = {
name: /^[A-Za-z]{5}$/,
age: integer().min(0).max(120),
gender: ['m', 'f']
};
Hmmm.... I prefer the second one :)
So let talk about the semantic-schema
. There are three critical concepts: schema
, sugar
and validator
:
A schema
is a describer describing what a data should be. It is a wrapper of JSON Schema providing a semantic way to declare it. Most of the time we don't directly use JSON Schema. But a method .normalize()
is provided to convert schema
to a JSON Schema object.
There are four types of schema
:
- number
- integer
- string
- boolean
- null
- object
- array
- one_of: means that the data should match one of the declared schemas
- invalid: means that the data will always be invalid no matter what value it is.
declaring a schema:
const {integer, string, object} = require('semantic-schema').schema;
let schema = object().properties({
foo: integer(),
bar: string()
}).requiredAll();
// valid: {foo: 1, bar: '1'}
// invalid: {foo: 1}, {foo: '1', bar: '1'}, 1, '1', []...
schema.normalize(); // convert it to a JSON Schema object.
A validator
will compile a schema
inside itself and provides a .validate()
method for you to check your target data.
const SemanticSchema = require('semantic-schema');
const {integer, string, object} = SemanticSchema.schema;
const Validator = SemanticSchema.validator;
let schema = object().properties({
foo: integer(),
bar: string()
}).requiredAll();
let validator = new Validator(schema); // or Validator.from(schema)
validator.validate({foo: 1, bar: '1'}); // true
validator.validate({foo: 1}); // false
validator.errorsText(); // error details for the last validation.
A sugar
is just a way to simplify your declaration of schema
. Your code will still work well without it. But it makes your code more clear.
Below is a collection of sugar:
sugar | equivalent |
---|---|
1 | integer().enum(1) |
1.1 | number().enum(1.1) |
'foo' | string().enum('foo') |
/^foo|bar$/ | string().pattern(/^foo|bar$/) |
true | boolean().enum(true) |
null | NULL() or empty() |
{foo: 1} | object().properties({foo: 1}).requiredAll() |
[1, 2, 3] | integer().enum(1, 2, 3) |
[1.1, 2.2, 3] | number().enum(1.1, 2.2, 3) |
['foo', 'bar'] | string().enum('foo', 'bar') |
[true, false] | boolean().enum(true, false) |
And you can use a sugar
just like a schema
:
const Validator = require('semantic-schema').validator;
let schema = {
foo: 1,
bar: ['hello', 'world'],
tar: /^[0-9A-F]{8}$/
};
let validator = Validator.from(schema);
validator.validate({foo: 1, bar: 'hello', tar: 'ABC12345'}); // true
validator.validate({foo: 0, bar: 'hello', tar: 'ABC12345'}); // false
validator.validate({foo: 1, bar: 'hi', tar: 'ABC12345'}); // false
validator.validate({foo: 1, bar: 'hi', tar: 'ZZZ'}); // false
If your schema
is complicated, I highly recommend you to use sugar
in your code. Consider a schema of an object with an 'if' condition:
const {integer, object, string} = require('semantic-schema').schema;
let schema = object()
.if.properties({type: 'student'})
.then.properties({
type: 'student',
major: ['music', 'math']
}).requiredAll()
.elseIf.properties({type: 'staff'})
.then.properties({
type: 'staff',
major: ['music', 'math'],
salary: integer()
}).requiredAll()
.else.invalid()
.endIf;
// without sugar it would be:
let schema = object()
.if.properties({type: string().enum('student')})
.then.properties({
type: string().enum('student'),
major: string().enum(['music', 'math'])
}).requiredAll()
.elseIf.properties({type: string().enum('staff')})
.then.properties({
type: string().enum('staff'),
major: string().enum(['music', 'math']),
salary: integer()
}).requiredAll()
.else.invalid()
.endIf;
// it also works, but obscure.