semantic-schema

write json schema in a graceful way.

Downloads in past

Stats

StarsIssuesVersionUpdatedCreatedSize
semantic-schema
22170.4.56 years ago6 years agoMinified + gzip package size for semantic-schema in KB

Readme

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
  • oneof: 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.