Tabbouleh -
A TypeScript library which generate JSON Schema (draft 7) from data class definition, in runtime.
-
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
- Get started
- Dependencies
- Motivation
- Note on draft used
- Use cases
- API
- How referencing works
- Examples
- Credits
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. EssentiallyJSONSchema7
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:
- http://json-schema.org/specification.html
- https://tools.ietf.org/html/draft-handrews-json-schema-validation-01
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.
@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 thedefinitions
of the root schema, -
address
field is now referencingUserAddress
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