semantic-schema

write json schema in a graceful way.


Keywords
json-schema, json, schema, object, semantic
License
ISC
Install
npm install semantic-schema@0.4.5

Documentation

Semantic Schema

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:

  1. Use JSON Schema as an underlying data structure describer;
  2. Get rid of the verbose grammar of JSON Schema;
  3. 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:

schema

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.

Validator

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.

Sugar

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.