fluent-json-schema

A fluent API to generate JSON schemas

README

fluent-json-schema


A fluent API to generate JSON schemas (draft-07) for Node.js and browser. Framework agnostic.

Features


- Fluent schema implements JSON Schema draft-07 standards
- Faster and shorter way to write a JSON Schema via a fluent API
- Runtime errors for invalid options or keywords misuse
- JavaScript constants can be used in the JSON schema (e.g. _enum_, _const_, _default_ ) avoiding discrepancies between model and schema
- TypeScript definitions
- Coverage 99%

Install


    npm i fluent-json-schema

or

    yarn add fluent-json-schema

Usage


  1. ```javascript
  2. const S = require('fluent-json-schema')

  3. const ROLES = {
  4.   ADMIN: 'ADMIN',
  5.   USER: 'USER',
  6. }

  7. const schema = S.object()
  8.   .id('http://foo/user')
  9.   .title('My First Fluent JSON Schema')
  10.   .description('A simple user')
  11.   .prop('email', S.string().format(S.FORMATS.EMAIL).required())
  12.   .prop('password', S.string().minLength(8).required())
  13.   .prop('role', S.string().enum(Object.values(ROLES)).default(ROLES.USER))
  14.   .prop(
  15.     'birthday',
  16.     S.raw({ type: 'string', format: 'date', formatMaximum: '2020-01-01' }) // formatMaximum is an AJV custom keywords
  17.   )
  18.   .definition(
  19.     'address',
  20.     S.object()
  21.       .id('#address')
  22.       .prop('line1', S.anyOf([S.string(), S.null()])) // JSON Schema nullable
  23.       .prop('line2', S.string().raw({ nullable: true })) // Open API / Swagger  nullable
  24.       .prop('country', S.string())
  25.       .prop('city', S.string())
  26.       .prop('zipcode', S.string())
  27.       .required(['line1', 'country', 'city', 'zipcode'])
  28.   )
  29.   .prop('address', S.ref('#address'))

  30. console.log(JSON.stringify(schema.valueOf(), undefined, 2))
  31. ```

Schema generated:

  1. ```json
  2. {
  3.   "$schema": "http://json-schema.org/draft-07/schema#",
  4.   "definitions": {
  5.     "address": {
  6.       "type": "object",
  7.       "$id": "#address",
  8.       "properties": {
  9.         "line1": {
  10.           "anyOf": [
  11.             {
  12.               "type": "string"
  13.             },
  14.             {
  15.               "type": "null"
  16.             }
  17.           ]
  18.         },
  19.         "line2": {
  20.           "type": "string",
  21.           "nullable": true
  22.         },
  23.         "country": {
  24.           "type": "string"
  25.         },
  26.         "city": {
  27.           "type": "string"
  28.         },
  29.         "zipcode": {
  30.           "type": "string"
  31.         }
  32.       },
  33.       "required": ["line1", "country", "city", "zipcode"]
  34.     }
  35.   },
  36.   "type": "object",
  37.   "$id": "http://foo/user",
  38.   "title": "My First Fluent JSON Schema",
  39.   "description": "A simple user",
  40.   "properties": {
  41.     "email": {
  42.       "type": "string",
  43.       "format": "email"
  44.     },
  45.     "password": {
  46.       "type": "string",
  47.       "minLength": 8
  48.     },
  49.     "birthday": {
  50.       "type": "string",
  51.       "format": "date",
  52.       "formatMaximum": "2020-01-01"
  53.     },
  54.     "role": {
  55.       "type": "string",
  56.       "enum": ["ADMIN", "USER"],
  57.       "default": "USER"
  58.     },
  59.     "address": {
  60.       "$ref": "#address"
  61.     }
  62.   },
  63.   "required": ["email", "password"]
  64. }
  65. ```

TypeScript


CommonJS


With "esModuleInterop": true activated in the tsconfig.json:

  1. ```typescript
  2. import S from 'fluent-json-schema'

  3. const schema = S.object()
  4.   .prop('foo', S.string())
  5.   .prop('bar', S.number())
  6.   .valueOf()
  7. ```

With "esModuleInterop": false in the tsconfig.json:

  1. ```typescript
  2. import * as S from 'fluent-json-schema'

  3. const schema = S.object()
  4.   .prop('foo', S.string())
  5.   .prop('bar', S.number())
  6.   .valueOf()
  7. ```

ESM


A named export is also available to work with native ESM modules:

  1. ```typescript
  2. import { S } from 'fluent-json-schema'

  3. const schema = S.object()
  4.   .prop('foo', S.string())
  5.   .prop('bar', S.number())
  6.   .valueOf()
  7. ```

Validation


Fluent schema does not validate a JSON schema. However, many libraries can do that for you.
Below a few examples using AJV:

    npm i ajv

or

    yarn add ajv

Validate an empty model


Snippet:

  1. ```javascript
  2. const ajv = new Ajv({ allErrors: true })
  3. const validate = ajv.compile(schema.valueOf())
  4. let user = {}
  5. let valid = validate(user)
  6. console.log({ valid }) //=> {valid: false}
  7. console.log(validate.errors) //=> {valid: false}
  8. ```

Output:

  1. ```
  2. {valid: false}
  3. errors: [
  4.   {
  5.     keyword: 'required',
  6.     dataPath: '',
  7.     schemaPath: '#/required',
  8.     params: { missingProperty: 'email' },
  9.     message: "should have required property 'email'",
  10.   },
  11.   {
  12.     keyword: 'required',
  13.     dataPath: '',
  14.     schemaPath: '#/required',
  15.     params: { missingProperty: 'password' },
  16.     message: "should have required property 'password'",
  17.   },
  18. ]

  19. ```

Validate a partially filled model


Snippet:

  1. ```javascript
  2. user = { email: 'test', password: 'password' }
  3. valid = validate(user)
  4. console.log({ valid })
  5. console.log(validate.errors)
  6. ```

Output:

  1. ```
  2. {valid: false}
  3. errors:
  4. [ { keyword: 'format',
  5.     dataPath: '.email',
  6.     schemaPath: '#/properties/email/format',
  7.     params: { format: 'email' },
  8.     message: 'should match format "email"' } ]

  9. ```

Validate a model with a wrong format attribute


Snippet:

  1. ```javascript
  2. user = { email: 'test@foo.com', password: 'password' }
  3. valid = validate(user)
  4. console.log({ valid })
  5. console.log('errors:', validate.errors)
  6. ```

Output:

  1. ```
  2. {valid: false}
  3. errors: [ { keyword: 'required',
  4.     dataPath: '.address',
  5.     schemaPath: '#definitions/address/required',
  6.     params: { missingProperty: 'country' },
  7.     message: 'should have required property \'country\'' },
  8.   { keyword: 'required',
  9.     dataPath: '.address',
  10.     schemaPath: '#definitions/address/required',
  11.     params: { missingProperty: 'city' },
  12.     message: 'should have required property \'city\'' },
  13.   { keyword: 'required',
  14.     dataPath: '.address',
  15.     schemaPath: '#definitions/address/required',
  16.     params: { missingProperty: 'zipcoce' },
  17.     message: 'should have required property \'zipcode\'' } ]
  18. ```

Valid model


Snippet:

  1. ```javascript
  2. user = { email: 'test@foo.com', password: 'password' }
  3. valid = validate(user)
  4. console.log({ valid })
  5. ```

Output:

    {valid: true}

Extend schema


Normally inheritance with JSON Schema is achieved with allOf. However when .additionalProperties(false) is used the validator won't
understand which properties come from the base schema. S.extend creates a schema merging the base into the new one so
that the validator knows all the properties because it is evaluating only a single schema.
For example, in a CRUD API POST /users could use the userBaseSchema rather than GET /users or PATCH /users use the userSchema
which contains the id, createdAt and updatedAt generated server side.

  1. ```js
  2. const S = require('fluent-json-schema')
  3. const userBaseSchema = S.object()
  4.   .additionalProperties(false)
  5.   .prop('username', S.string())
  6.   .prop('password', S.string())

  7. const userSchema = S.object()
  8.   .prop('id', S.string().format('uuid'))
  9.   .prop('createdAt', S.string().format('time'))
  10.   .prop('updatedAt', S.string().format('time'))
  11.   .extend(userBaseSchema)

  12. console.log(userSchema)
  13. ```

Selecting certain properties of your schema


In addition to extending schemas, it is also possible to reduce them into smaller schemas. This comes in handy
when you have a large Fluent Schema, and would like to re-use some of its properties.

Select only properties you want to keep.

  1. ```js
  2. const S = require('fluent-json-schema')
  3. const userSchema = S.object()
  4.   .prop('username', S.string())
  5.   .prop('password', S.string())
  6.   .prop('id', S.string().format('uuid'))
  7.   .prop('createdAt', S.string().format('time'))
  8.   .prop('updatedAt', S.string().format('time'))

  9. const loginSchema = userSchema.only(['username', 'password'])
  10. ```

Or remove properties you dont want to keep.

  1. ```js
  2. const S = require('fluent-json-schema')
  3. const personSchema = S.object()
  4.   .prop('name', S.string())
  5.   .prop('age', S.number())
  6.   .prop('id', S.string().format('uuid'))
  7.   .prop('createdAt', S.string().format('time'))
  8.   .prop('updatedAt', S.string().format('time'))

  9. const bodySchema = personSchema.without(['createdAt', 'updatedAt'])
  10. ```

Detect Fluent Schema objects


Every Fluent Schema object contains a boolean isFluentSchema. In this way, you can write your own utilities that understands the Fluent Schema API and improve the user experience of your tool.

  1. ```js
  2. const S = require('fluent-json-schema')
  3. const schema = S.object().prop('foo', S.string()).prop('bar', S.number())
  4. console.log(schema.isFluentSchema) // true
  5. ```

Documentation



Acknowledgments


Thanks to Matteo Collina for pushing me to implement this utility! 🙏

Related projects


- JSON Schema Draft 7
- Understanding JSON Schema (despite is referring to draft 6 the guide still good to grasp the main concepts)
- AJV JSON Schema validator
- jsonschema.net an online JSON Schema visual editor (it doesn't support advance features)

Licence


Licensed under MIT.