tabbouleh
TypeScript icon, indicating that this package has built-in type declarations

1.0.2 • Public • Published

Tabbouleh - License NPM version npm bundle size contribute

A TypeScript library which generate JSON Schema (draft 7) from data class definition, in runtime.

Travis Coverage Status Dev Dependencies Quality Gate Status Maintainability Rating

  • Class-based - Structure your data definitions with classes, in which you put your JSON Schema properties. No need to create other types, classes or variables.

  • Decorators - Define JSON Schema of data fields with decorators, for readability & understandability.

  • Field type inference - Type of the field JSON Schema can be inferred from its TypeScript type.

  • Non-opinionated - No link to any other libraries. Choose the validator you want, the form generator you want, they just have to work with JSON Schema format which is quite generic.

  • Back & Front - Tabbouleh works in the same way with Node or in a Browser environment, it doesn't matter !



Install

npm install tabbouleh --save

For enable TypeScript decorators, your tsconfig.json needs the following flags:

"experimentalDecorators": true,
"emitDecoratorMetadata": true

Get started

Let's imagine a login case.

Define a data structure

The user will login with:

  • its email - It must follow the email format.
  • its password - It must have at least 6 chars.

All these fields are required.

import { JSONSchema, JSONString } from 'tabbouleh';

@JSONSchema<LoginData>({
  required: ['email', 'password']
})
export class LoginData {

  @JSONString({
    format: 'email'
  })
  email: string;

  @JSONString({
    minLength: 6
  })
  password: string;

}

Generate its JSON Schema

From our data structure, we generate its JSON Schema.

import Tabbouleh from 'tabbouleh';
import { JSONSchema7 } from 'json-schema';

const schema: JSONSchema7 = Tabbouleh.generateJSONSchema(LoginData);

And our schema looks like...

{
  "type": "object",
  "required": [
    "email",
    "password"
  ],
  "properties": {
    "email": {
      "type": "string",
      "format": "email"
    },
    "password": {
      "type": "string",
      "minLength": 6
    }
  }
}

Dependencies

Tabbouleh has 2 dependencies:

  • reflect-metadata, for decorators.
  • json-schema, for schema types. Essentially JSONSchema7 which is the type of schemas generated by Tabbouleh.

Motivation

To understand my motivation behind Tabbouleh we have to simulate an user data input process. Like a login.

Let's list the steps:

  • Define the data structure (like with a type or class). We have an username and a password.

  • [front-end] Generate a form with an input for each of the data fields, which one need to have some rules (required, minLength, ...).

  • [front-end] On form submit, validate the data, check that it follows all the rules.

  • [front-end] Then send the data to the back-end.

  • [back-end] On data receipt, validate the data, again (no trust with front).

The problem

If you ever developed this kind of process you may know the inconsistency of the binding between the data and each of these steps.

When we create the form, there is no concrete link between inputs and the data structure. We have to create an input for each data field, give it its rules in a HTML way.

Then on form submit, the data must be generated from inputs values (from FormData object), and validated with the data rules, for each field.

Again, when the back-end has the data, it validates it with the same rules.

The definition of these rules and there validation may be programmatically done, in each of these steps with lot of redundancy, fat & ugly code, poor maintainability, and too much time.

My solution

I wanted a way to define all these rules easily, elegantly, without a ton of code. When I define my data structure, I define its validation rules in the same place. And from my data structure, I get my related JSON Schema. It's simple, it's how Tabbouleh works.

Then I use the generated JSON Schema for generate my form, and validate the data submitted. Easily.

The JSON Schema format is normalized and handled by many data validators and form generators.

But careful, Tabbouleh will not validate your data, or generate your form. It'll just do the first step of these: generate the JSON Schema, which can be used for these purposes. Check the use cases for more.

Note on draft used

Tabbouleh actually uses the draft 7 of JSON Schema specification.

For more:

Use cases

Data validation

You can see the use of Tabbouleh with AJV in this dedicated repo: tabbouleh-sample-ajv.

Form generation

You can see the use of Tabbouleh with react-jsonschema-form in this dedicated repo: tabbouleh-sample-rjsf.

An alternative of react-jsonschema-form: uniforms.

API

Schema definitions are made in your data class, with decorators.

@JSONSchema

The only decorator for the class head. It defines the root schema properties.

More infos on which fields you can use. [10]

@JSONSchema<LoginData>({
  $id: "https://example.com/login.json",
  $schema: "http://json-schema.org/draft-07/schema#",
  title: "Login data",
  description: "Data required form user login",
  required: ['email', 'password']
})
export class LoginData {

  @JSONString
  email: string;

  @JSONString
  password: string;

}
@JSONSchema
export class LoginData {

  @JSONString
  email: string;

  @JSONString
  password: string;

}

@JSONProperty

Field decorator which doesn't define the schema type. If not defined it will be inferred from the field type.

Depending on the type given, see the corresponding decorator to know which fields are allowed. Also, more infos on which fields you can use. [6.1]

@JSONSchema
export class LoginData {

  @JSONProperty
  email: string;

  @JSONProperty<JSONEntityString>({
    type: 'string',
    minLength: 6
  })
  password: string;

}

@JSONString

Field decorator for string type.

More infos on which fields you can use. [6.3]

@JSONSchema
export class LoginData {

  @JSONString({
    format: 'email',
    maxLength: 64
  })
  email: string;

  @JSONString
  password: string;

}

@JSONNumber & @JSONInteger

Field decorators for number and integer types. They share the same fields.

More infos on which fields you can use. [6.2]

@JSONSchema
export class UserData {

  @JSONInteger({
    minimum: 0
  })
  age: number;

  @JSONNumber
  percentCompleted: number;

}

@JSONBoolean

Field decorator for boolean type.

@JSONSchema
export class UserData {

  @JSONBoolean
  active: boolean;

}

@JSONObject

Field decorator for object type.

More infos on which fields you can use. [6.5]

@JSONSchema
export class UserData {

  @JSONObject({
    properties: {
      street: {
        type: 'string'
      },
      city: {
        type: 'string'
      }
    }
  })
  address: object;

}

Class reference

With this decorator you can reference an other schema class.

Wrap your class !

@JSONSchema
export class UserData {

  @JSONObject(() => UserAddress)
  address: UserAddress;

}
@JSONSchema
class UserAddress {

  @JSONString
  street: string;

  @JSONString
  city: string;

}

@JSONArray

Field decorator for array type.

More infos on which fields you can use. [6.4]

@JSONSchema
class UserData {

  @JSONArray({
    items: {
      type: 'integer'
    }
  })
  childrenAges: number[];

}

Class reference

As with @JSONObject you can reference an other schema class.

@JSONSchema
class UserData {

  @JSONArray(() => UserData)
  children: UserData[];

}

You may want to add some properties in addition of the reference, for that put the reference in the items property.

@JSONSchema
class UserData {

  @JSONArray({
    items: () => UserData,
    maxItems: 4,
    minItems: 0
  })
  children: UserData[];

}

How referencing works

Let's take one of the previous example.

@JSONSchema
export class UserData {

  @JSONObject(() => UserAddress)
  address: UserAddress;

}
@JSONSchema
class UserAddress {

  @JSONString
  street: string;

  @JSONString
  city: string;

}

The JSON result will be:

{
  "type": "object",
  
  "definitions": {
    "_UserAddress_": {
      "type": "object",
      "properties": {
        "street": {
          "type": "string"
        },
        "city": {
          "type": "string"
        }
      }
    }
  },
  
  "properties": {
    
    "address": {
      "$ref": "#/definitions/_UserAddress_"
    }
    
  }
}

You can see that:

  • UserAdress schema was put in the definitions of the root schema,
  • address field is now referencing UserAddress by using the $ref field.

A class reference is always translated as a schema reference with the use of the $ref. Because of multiple same references optimization, and because of circular reference handling.

Wrap the class !

You have to wrap the target in a function, like () => MyClass.

It is required because of the case of circular referencing which may cause an undefined value instead of the referenced class.

Examples

You can find all examples used in /examples.

Credits

This library was created with typescript-library-starter.

So, why tabbouleh ?

Hummus was already taken :shipit:

Package Sidebar

Install

npm i tabbouleh

Weekly Downloads

0

Version

1.0.2

License

MIT

Unpacked Size

447 kB

Total Files

67

Last publish

Collaborators

  • chnapy