Nupital Pomp Mesmerises

    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:

    Install

    npm i tabbouleh

    DownloadsWeekly Downloads

    1

    Version

    1.0.2

    License

    MIT

    Unpacked Size

    447 kB

    Total Files

    67

    Last publish

    Collaborators

    • chnapy