GithubHelp home page GithubHelp logo

fastify / fluent-json-schema Goto Github PK

View Code? Open in Web Editor NEW
481.0 21.0 55.0 583 KB

A fluent API to generate JSON schemas

License: MIT License

JavaScript 97.01% TypeScript 2.95% Shell 0.04%
fastify-library

fluent-json-schema's Introduction

fluent-json-schema

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

view on npm Coverage Status JavaScript Style Guide

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

const S = require('fluent-json-schema')

const ROLES = {
  ADMIN: 'ADMIN',
  USER: 'USER',
}

const schema = S.object()
  .id('http://foo/user')
  .title('My First Fluent JSON Schema')
  .description('A simple user')
  .prop('email', S.string().format(S.FORMATS.EMAIL).required())
  .prop('password', S.string().minLength(8).required())
  .prop('role', S.string().enum(Object.values(ROLES)).default(ROLES.USER))
  .prop(
    'birthday',
    S.raw({ type: 'string', format: 'date', formatMaximum: '2020-01-01' }) // formatMaximum is an AJV custom keywords
  )
  .definition(
    'address',
    S.object()
      .id('#address')
      .prop('line1', S.anyOf([S.string(), S.null()])) // JSON Schema nullable
      .prop('line2', S.string().raw({ nullable: true })) // Open API / Swagger  nullable
      .prop('country', S.string())
      .prop('city', S.string())
      .prop('zipcode', S.string())
      .required(['line1', 'country', 'city', 'zipcode'])
  )
  .prop('address', S.ref('#address'))

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

Schema generated:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "definitions": {
    "address": {
      "type": "object",
      "$id": "#address",
      "properties": {
        "line1": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ]
        },
        "line2": {
          "type": "string",
          "nullable": true
        },
        "country": {
          "type": "string"
        },
        "city": {
          "type": "string"
        },
        "zipcode": {
          "type": "string"
        }
      },
      "required": ["line1", "country", "city", "zipcode"]
    }
  },
  "type": "object",
  "$id": "http://foo/user",
  "title": "My First Fluent JSON Schema",
  "description": "A simple user",
  "properties": {
    "email": {
      "type": "string",
      "format": "email"
    },
    "password": {
      "type": "string",
      "minLength": 8
    },
    "birthday": {
      "type": "string",
      "format": "date",
      "formatMaximum": "2020-01-01"
    },
    "role": {
      "type": "string",
      "enum": ["ADMIN", "USER"],
      "default": "USER"
    },
    "address": {
      "$ref": "#address"
    }
  },
  "required": ["email", "password"]
}

TypeScript

CommonJS

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

import S from 'fluent-json-schema'

const schema = S.object()
  .prop('foo', S.string())
  .prop('bar', S.number())
  .valueOf()

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

import * as S from 'fluent-json-schema'

const schema = S.object()
  .prop('foo', S.string())
  .prop('bar', S.number())
  .valueOf()

ESM

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

import { S } from 'fluent-json-schema'

const schema = S.object()
  .prop('foo', S.string())
  .prop('bar', S.number())
  .valueOf()

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:

const ajv = new Ajv({ allErrors: true })
const validate = ajv.compile(schema.valueOf())
let user = {}
let valid = validate(user)
console.log({ valid }) //=> {valid: false}
console.log(validate.errors) //=> {valid: false}

Output:

{valid: false}
errors: [
  {
    keyword: 'required',
    dataPath: '',
    schemaPath: '#/required',
    params: { missingProperty: 'email' },
    message: "should have required property 'email'",
  },
  {
    keyword: 'required',
    dataPath: '',
    schemaPath: '#/required',
    params: { missingProperty: 'password' },
    message: "should have required property 'password'",
  },
]

Validate a partially filled model

Snippet:

user = { email: 'test', password: 'password' }
valid = validate(user)
console.log({ valid })
console.log(validate.errors)

Output:

{valid: false}
errors:
[ { keyword: 'format',
    dataPath: '.email',
    schemaPath: '#/properties/email/format',
    params: { format: 'email' },
    message: 'should match format "email"' } ]

Validate a model with a wrong format attribute

Snippet:

user = { email: '[email protected]', password: 'password' }
valid = validate(user)
console.log({ valid })
console.log('errors:', validate.errors)

Output:

{valid: false}
errors: [ { keyword: 'required',
    dataPath: '.address',
    schemaPath: '#definitions/address/required',
    params: { missingProperty: 'country' },
    message: 'should have required property \'country\'' },
  { keyword: 'required',
    dataPath: '.address',
    schemaPath: '#definitions/address/required',
    params: { missingProperty: 'city' },
    message: 'should have required property \'city\'' },
  { keyword: 'required',
    dataPath: '.address',
    schemaPath: '#definitions/address/required',
    params: { missingProperty: 'zipcoce' },
    message: 'should have required property \'zipcode\'' } ]

Valid model

Snippet:

user = { email: '[email protected]', password: 'password' }
valid = validate(user)
console.log({ valid })

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.

const S = require('fluent-json-schema')
const userBaseSchema = S.object()
  .additionalProperties(false)
  .prop('username', S.string())
  .prop('password', S.string())

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

console.log(userSchema)

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.

const S = require('fluent-json-schema')
const userSchema = S.object()
  .prop('username', S.string())
  .prop('password', S.string())
  .prop('id', S.string().format('uuid'))
  .prop('createdAt', S.string().format('time'))
  .prop('updatedAt', S.string().format('time'))

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

Or remove properties you dont want to keep.

const S = require('fluent-json-schema')
const personSchema = S.object()
  .prop('name', S.string())
  .prop('age', S.number())
  .prop('id', S.string().format('uuid'))
  .prop('createdAt', S.string().format('time'))
  .prop('updatedAt', S.string().format('time'))

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

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.

const S = require('fluent-json-schema')
const schema = S.object().prop('foo', S.string()).prop('bar', S.number())
console.log(schema.isFluentSchema) // true

Documentation

Acknowledgments

Thanks to Matteo Collina for pushing me to implement this utility! ๐Ÿ™

Related projects

Licence

Licensed under MIT.

fluent-json-schema's People

Contributors

aboutlo avatar boyander avatar chengluyu avatar climba03003 avatar cm-ayf avatar dancastillo avatar delvedor avatar dependabot[bot] avatar dfee avatar dodiego avatar eomm avatar erfanium avatar esatterwhite avatar fdawgs avatar greguz avatar gurgunday avatar jillesme avatar jnv avatar joaopedrocampos avatar jsumners avatar leorossi avatar mcollina avatar mdeltito avatar rrufus avatar salmanm avatar simoneb avatar simonplend avatar svile avatar uzlopak avatar victortosts avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

fluent-json-schema's Issues

v1.0.2 breaks required properties array when extend is used

๐Ÿ’ฅ Regression Report

Version 1.0.2 breaks the required properties array merge when extending a schema from another.

Last working version

Worked up to version: 1.0.1

Stopped working in version: 1.0.2

To Reproduce

Steps to reproduce the behavior:

const S = require('fluent-schema');

const entityBase = S.object()
  .prop('id', S.string().format('uuid').required())
  .prop('version', S.string().format('date-time').required())
  .prop('label', S.string().required())
  .prop('createdBy', S.string().format('uuid').required())
  .prop('createdOn', S.string().format('date-time').required())
  .prop('modifiedBy', S.string().format('uuid').required())
  .prop('modifiedOn', S.string().format('date-time').required());

const test = S.object()
  .title('test schema')
  .additionalProperties(false)
  .prop('parent', S.string().format('uuid').required())
  .extend(entityBase);

module.exports = test;

Expected behavior

In version 1.0.1 the required properties array in the json schema is produced correctly:

"required": [
  "id",
  "version",
  "label",
  "createdBy",
  "createdOn",
  "modifiedBy",
  "modifiedOn",
  "parent"
]

In version 1.0.2 the required properties array gets messed up:

"required": [
  {
    "0": "p",
    "1": "a",
    "2": "r",
    "3": "e",
    "4": "n",
    "5": "t"
  },
  "version",
  "label",
  "createdBy",
  "createdOn",
  "modifiedBy",
  "modifiedOn"
]

Likely a result of this PR.

Using version 1.0.1 until this is fixed ๐Ÿ‘
Btw, loving this package so far, keep up the awesome work! If I have some time I might look into fixing this.

Your Environment

  • node version: 12
  • os: Mac

ifThenElse block prop chain

๐Ÿ› Bug Report

Hi, little issue with ifThenElse chaining.

The stacktrace:

  .prop('token', S.mixed([S.TYPES.STRING, S.TYPES.NULL]))
   ^
TypeError: S.object(...).prop(...).ifThenElse(...).prop is not a function

To Reproduce

If I move the token prop before the ifThenElse all works

const schema = S.object()
  .prop('deviceType')
  .ifThenElse(
    S.object().prop('token', S.null()),
    S.object()
      .prop('deviceType', S.integer())
      .required(),
    S.object()
      .prop('deviceType', S.mixed([S.TYPES.STRING, S.TYPES.NULL]))
      .required()
  )
  .prop('token', S.mixed([S.TYPES.STRING, S.TYPES.NULL]))
  .valueOf()

console.log(JSON.stringify(schema, null, 2))

Expected behavior

After the ifThenElse I should be able to add other .prop

Duplicate `required()` generates invalid schema

๐Ÿ› Bug Report

Calling required() twice on same prop duplicates key in required schema array required:[<prop>,<prop>] generating an invalid schema.

To Reproduce

Steps to reproduce the behavior:

const S = require('fluent-json-schema')

const schema = S.object()
  .prop('email', S.string()).required().required()
  
console.log(JSON.stringify(schema.valueOf(), undefined, 2))

causes invalid schema with duplicate prop in required array

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "email": {
      "type": "string"
    }
  },
  "required": [
    "email",
    "email"
  ]
}

https://replit.com/@boyander/duplicatedrequiredprop

Expected behavior

Either throw an error if using require() twice or generate the required:[] array using a Set

Your Environment

  • node version: 12
  • os: Mac

Also, congrats for this awesome package. We use it a lot at @core-school

IfThen with constant

๐Ÿ’ฌ Questions and Help

I don't find if it is possible to write this json schema:

{
  "type": "object",
  "properties": {
    "type": {
      "type": "string",
      "enum": ["AS", "RP"]
    }
  },
  "if": {
    "properties": { "type": { "const": "RP" } }
  },
  "then": {
    "properties": { "issueType": { "type": "string" } },
    "required": [ "issueType" ]
  }
}

I'm trying the code:

const S = require('fluent-schema')

const fs = S.object()
  .prop('type', S.string().enum(['AS', 'CS', 'RP']))
  .ifThen(
    // S.object().prop('type', S.string().default('RP')),
    S.object().prop('type', 'RP'),
    S.object().prop('issueType', S.string().required())
  )

console.log(JSON.stringify(fs.valueOf(), null, 2))

But I get:

  "if": {
    "properties": {
      "type": {
        "0": "R",
        "1": "P"
      }
    }
  },

or

  "if": {
    "properties": {
      "type": {
        "type": "string",
        "default": "RP"
      }
    }
  },

Should I use S.raw?

ObjectSchema.required without parameters wrongly set the last prop as required

๐Ÿ› Bug Report

ObjectSchema.required() should add itself as a required property to its parent.
However, calling it will wrongly make the last prop defined as required

To Reproduce

Steps to reproduce the behavior:

console.log(
    S.object()
      .prop(
        'perm',
        S.object()
          .prop('admin', S.boolean())
          .prop('comment', S.boolean())
          .required()
      )
      .valueOf()
  )

will output:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "perm": {
      "type": "object",
      "properties": {
        "admin": {
          "type": "boolean"
        },
        "comment": {
          "type": "boolean"
        }
      },
      "required": [
        "comment"
      ]
    }
  }
}

Expected behavior

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "perm": {
      "type": "object",
      "properties": {
        "admin": {
          "type": "boolean"
        },
        "comment": {
          "type": "boolean"
        }
      }
    }
  },
  "required": [
    "perm"
  ]
}

Your Environment

  • node version: 14
  • fastify version: >=2.0.0
  • os: Windows

Node 6 support

Hi,
adding some test to verify the usage of fluent-schema with fastify, I found fluent-schema doesn't work with node.js 6.x. Since it is supported by fastify v2, it would be great to support it too.

Here fastify/fastify#1485 (comment) more details with CI failure.

I test it also locally and I get the same behaviour with node.js 6 and 7:

image

With node.js >= 8 all is fine.

I'm not a TypeScript dev, but if you would like to give me some information I would be glad to help if it is needed.

Forward slashes are ignored when specifying string pattern

๐Ÿ› Bug Report

It seems that in attempts to auto escape the regex for json compatibility the library strips out (or ignores) escaped /

To Reproduce

Steps to reproduce the behavior:

import S from 'fluent-json-schema';

const cronlike = S.string().pattern(
  /^((((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*)\s?){5,6})$/,
);
console.log(cronlike.valueOf());

const cronlikeRegex = RegExp(
  /^((((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*)\s?){5,6})$/,
);
console.log(cronlikeRegex);

Expected behavior

Forward slashes should also be escaped.

{
  '$schema': 'http://json-schema.org/draft-07/schema#',
  type: 'string',
  pattern: '^((((\\d+,)+\\d+|(\\d+(\\|-)\\d+)|\\d+|\\*)\\s?){5,6})$/'
}
/^((((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*)\s?){5,6})$/

Your Environment

  • node version: v14.16.1
  • fluent-json-schema: 2.0.4
  • os: Mac

Confusing documentation about `required`

None of the issue template is about documentation, so I opened a blank issue.

In fastify docs (https://github.com/fastify/fastify/blame/master/docs/Fluent-Schema.md#L79), required is called after prop was called (e.g. S.object().prop('x', S.string()).required()).

In fluent-schema's docs, required is called either inside of prop (e.g. S.object().prop('x', S.string().required())) or outside but with arguments (e.g. S.object().prop('x', S.string()).required(['x'])).

So far, we have 3 different usage of required. All of them works on the lastest fluent-schema (see REPL results below). I think docs should illustrate all possible usage of a function. How could we improve?

> S.object().prop('y', S.string()).prop('x', S.string()).required().valueOf()
{
  '$schema': 'http://json-schema.org/draft-07/schema#',
  type: 'object',
  properties: { y: { type: 'string' }, x: { type: 'string' } },
  required: [ 'x' ]
}
> S.object().prop('y', S.string()).prop('x', S.string().required()).valueOf()
{
  '$schema': 'http://json-schema.org/draft-07/schema#',
  type: 'object',
  properties: { y: { type: 'string' }, x: { type: 'string' } },
  required: [ 'x' ]
}
> S.object().prop('y', S.string()).prop('x', S.string()).required(['x']).valueOf()
{
  '$schema': 'http://json-schema.org/draft-07/schema#',
  type: 'object',
  properties: { y: { type: 'string' }, x: { type: 'string' } },
  required: [ 'x' ]
}

Action required: Greenkeeper could not be activated ๐Ÿšจ

๐Ÿšจ You need to enable Continuous Integration on Greenkeeper branches of this repository. ๐Ÿšจ

To enable Greenkeeper, you need to make sure that a commit status is reported on all branches. This is required by Greenkeeper because it uses your CI build statuses to figure out when to notify you about breaking changes.

Since we didnโ€™t receive a CI status on the greenkeeper/initial branch, itโ€™s possible that you donโ€™t have CI set up yet.
We recommend using:

If you have already set up a CI for this repository, you might need to check how itโ€™s configured. Make sure it is set to run on all new branches. If you donโ€™t want it to run on absolutely every branch, you can whitelist branches starting with greenkeeper/.

Once you have installed and configured CI on this repository correctly, youโ€™ll need to re-trigger Greenkeeperโ€™s initial pull request. To do this, please click the 'fix repo' button on account.greenkeeper.io.

Extend schema does not extend properties, but overrides them

Following example will override title attribute because prop in NEW_SCHEMA is overriding one from SCHEMA_BASE.

To Reproduce

const SCHEMA_BASE = S.object()
  .prop(
    'reason',
    S.string()
      .title('Reason 1')
  )

const NEW_SCHEMA = S.object()
  .extend(SCHEMA_BASE)
  .prop(
    'reason',
    S.string()
      .minLength(1)
  )
  

Expected behavior

Props should be extended with with only changed attributes.

Environment

  • node version: 10
  • fastify version: >=2.0.0
  • os: Mac

Conditial required

How to achive conditional required e.g. for Email JSON object:

Either prop Body needs to be defined or Template and Model props need to be defined?

Something amoung these lines:

const email = S.object()
 .prop('from', S.string().required())
 .prop('to', S.string().required())
 .prop('subject', S.string().required())
 .prop('body', S.string().conditionallyRequired('if template === undefined')
 .prop('template', S.string().conditionallyRequired('if body === undefined')
 .prop('model', S.object().conditionallyRequired('if template !== undefined')

Is there a way to achive this.

Thanks.

Compute TypeScript type

๐Ÿš€ Feature Proposal

I think it will be possible and convenient to add a way to expose the TypeScript type of each object schema

Motivation

I started, like the doc of fastify suggest, to use fluent-schema (which is awesome!) and json-schema-to-typescript to generate types... in separate files.

Also this method could be ok for those who prefer having separate files, separate schema & types, I am not a big fan of depending on a script to re-generate those types. And even with a banner saying do not modify this file, update the schema instead, this could be error prone.

Another way to handle the case would be to add the capacity to fluent-schema to generate types directly. It would allow users to export a schema and the matching interface or type from the same module, not depending on scripts, not having several files, not having a type written that can be modified, not having another library, ... You got the point, it would be just simpler :)

Example

To my opinion, a good example of API for this case is mobx-state-tree which allow to generate Interface or Type from a model

import {Instance} from 'mobx-state-tree'

const Todo = types
    .model({
        title: "hello",
        done: false,
    })

interface ITodo extends Instance<typeof Todo> {}
// => {title: string, done: boolean}

type TTodo = typeof Todo.Type
// => {title: string, done: boolean}

The helper Instance is from MobX State Tree here, not a TypeScript helper.

The bad news is, I don't know how to do this myself, but I can learn and help if needed ๐Ÿ™

How to verify the file upload field?

Current Problem

const upLoadBodySchema = S.object()
  .prop('fileName', S.string())
  .prop('file', S.required().raw({ type: 'string', format: 'binary'}));  // There's going to be an error
Service Startup Failed! FastifyError [Error]: Failed building the validation schema for POST: /upload, due to error unknown format "binary" is used in schema at path "#/properties/file"
    at Boot.<anonymous> (F:\node-server-fastify\node_modules\fastify\lib\route.js:261:19)
    at Object.onceWrapper (events.js:421:28)
    at Boot.emit (events.js:327:22)
    at F:\node-server-fastify\node_modules\avvio\boot.js:153:12
    at F:\node-server-fastify\node_modules\avvio\plugin.js:269:7
    at done (F:\node-server-fastify\node_modules\avvio\plugin.js:201:5)
    at check (F:\node-server-fastify\node_modules\avvio\plugin.js:225:9)
    at internal/process/task_queues.js:153:7
    at AsyncResource.runInAsyncScope (async_hooks.js:186:9)
    at AsyncResource.runMicrotask (internal/process/task_queues.js:150:8) {
  code: 'FST_ERR_SCH_VALIDATION_BUILD',
  statusCode: 500
}

Expected behavior

How should I correctly verify the uploaded file?

Refer to the Swagger documentation for the above
https://swagger.io/docs/specification/describing-request-body/file-upload/?sbsearch=file

Environment

  • node version: v12.18.0
  • fastify version: >=3.1.1
  • os: Windows
  • fluent-schema version: >=1.0.4

sugar feature: string length

๐Ÿš€ Feature Proposal

Add a .length(24) method as a shortcut for min + max length.

Motivation

Right now to add a check for a string I need to write:

S.string().minLength(24).maxLength(24)

Example

S.string().length(24)

WDYT?

Typescript reserved words

In the Typescript definition:

If we change the naming it's a breaking change in the js implementation as well

extend api wrong typings

๐Ÿ› Bug Report

After the Schema extend other Schema, some of the api will be missing.

To Reproduce

Steps to reproduce the behavior:

const Base = S.object()
  .prop('foo', S.string())

const Extend = S.object()
  .prop('bar', S.string())
  .extend(Base)
  .required(['foo', 'bar']) // missing endpoint here

Expected behavior

const Base = S.object()
  .prop('foo', S.string())

const Extend = S.object()
  .prop('bar', S.string())
  .extend(Base)
  .required(['foo', 'bar']) // without error

Your Environment

  • node version: 14
  • fastify version: >=2.0.0
  • os: Linux
  • any other relevant information

TS Multiple successive .extend

๐Ÿ’ฅ Regression Report

I am unable to compile my TS fluent schemas with two successive .extend.

Last working version

Worked up to version: 1.0.4
Stopped working in version: 2.0.3

To Reproduce

Steps to reproduce the behavior:

import S from 'fluent-json-schema';

export const BaseSchema = S
  .object()
  .additionalProperties(false)
  .title('Base Schema')
  .description('Base')
  .id('base')
  .prop('baseProp', S.string());

export const ChildrenSchema = S
  .object()
  .additionalProperties(false)
  .title('Children Schema')
  .description('Children')
  .id('children')
  .prop('childrenProp', S.string())
  .extend(BaseSchema);

export const GrandChildrenSchema = S
  .object()
  .additionalProperties(false)
  .title('Grand Children Schema')
  .description('Grand Children')
  .id('grandChildren')
  .prop('grandChildrenProp', S.string())
  .extend(ChildrenSchema);                     // Error
  // .extend(BaseSchema)                       // Works

Expected behavior

I was expecting to be able to extend successively fluent schemas.

It made me wonder if this is possible to extend schemas this way. If not, how do you recommend to do this ?

error TS2345: Argument of type 'Pick<ObjectSchema, "isFluentSchema" | "extend">' is not assignable to parameter of type 'ObjectSchema'.
  Type 'Pick<ObjectSchema, "isFluentSchema" | "extend">' is missing the following properties from type 'ObjectSchema': definition, prop, additionalProperties, maxProperties, and 24 more.

.extend(ChildrenSchema); // Error

Your Environment

  • node version: 15.8.0
  • fastify version: 3.12.0
  • os: Linux

Failed Building Schema: Cannot use 'in' operator to search for 'maxProperties' in true

Forgive me for posting this question in issues; I was unable to access the Gitter channel for Fastify.

I'm experiencing an issue when I attempt to generate a 200 response schema using fluent-schema for a GET endpoint in my app. When I wrote the schema using regular JSON schema, it worked just fine. This is what I had before:

const getApplicationStatusesSchema = {
    headers: {
      type: 'object',
      properties: {
        'auth-token': {
          type: 'string',
        },
      },
      required: ['auth-token'],
    },
    response: {
      '200': {
        type: 'array',
        items: {
          type: 'object',
          properties: {
            id: {
              type: 'string',
            },
            applicationId: {
              type: 'string',
            },
            customerNumber: {
              type: 'string',
            },
            status: {
              type: 'string',
            },
            url: {
              type: 'string',
              format: 'url',
              nullable: true,
            },
          },
        },
      },
    },
  }

After a teammate transitioned to using fluent-schema for route schema, I rewrote to this (combined files for clarity):

const S = require('fluent-schema')

const statusOptions = [
  'incomplete',
  'approved',
  'action required',
  'declined',
  'pending',
  'withdrawn',
  // TODO: more?
]

module.exports = {
    headers: S.object().prop('auth-token', S.string().required()),
    response: S.object().prop(
      '200',
      S.array().items(
        S.object()
          .prop('id', S.string())
          .prop('applicationId', S.string().required())
          .prop('customerNumber', S.string().required())
          .prop('status', S.enum(statusOptions).required())
          .prop(
            'url',
            S.anyOf([S.string().format(S.FORMATS.URL), S.null()]).required()
          )
      )
    ),
  }

Now when I start the app, I get this error:

FastifyError [FST_ERR_SCH_BUILD]: FST_ERR_SCH_BUILD: Failed building the schema for GET: /online-credit-apps/status, due error Cannot use 'in' operator to search for 'maxProperties' in true

The issue is most definitely coming from the response block above. When I replace S.array() with S.string() or something similar the app does not crash. I'm unsure how to get Fastify to log anything else in the error stack. Does anyone see an issue with this? What else can I provide to help answer this question? Thanks in advance!

Environment

  • node version: 12.16.3
  • fastify version: 2.13.0
  • fluent-schema version: 1.0.2
  • os: Mac

Support required of a nested FluentSchema

I think is more idiomatic to declare required directly in the FluentSchema related to a nested prop

currently

const schema = FluentSchema()
  .prop('address', FluentSchema().asString())
  .required()

then

const schema = FluentSchema()
  .prop('address', FluentSchema().asString().required())

Chain call order

Let's take this simple example:

const S = require('fluent-schema')

console.log(
  S.object()
    .prop(
      'name',
      S.string()
        .required()
        .minLength(3)
        .maxLength(35)
    )
    .valueOf()
)

From what I understand, it should print the schema on the command line. What it currently does is:

/tmp/node_modules/fluent-schema/src/utils.js:154
  return options.factory({ schema: { ...schema, [key]: value }, ...options })
                 ^

TypeError: options.factory is not a function
    at setAttribute (/tmp/node_modules/fluent-schema/src/utils.js:154:18)
    at Object.minLength (/tmp/node_modules/fluent-schema/src/StringSchema.js:49:12)
    at Object.<anonymous> (/tmp/test.js:9:10)
    at Module._compile (internal/modules/cjs/loader.js:689:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10)
    at Module.load (internal/modules/cjs/loader.js:599:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:538:12)
    at Function.Module._load (internal/modules/cjs/loader.js:530:3)
    at Function.Module.runMain (internal/modules/cjs/loader.js:742:12)
    at startup (internal/bootstrap/node.js:283:19)

However, if I move the .required() call to the end of the prop definition, it works as expected.

Readme usage example not correct

The usage example in the Readme complains about required() has to be under prop.

  .prop(
    'email',
    FluentSchema()
      .asString()
      .format(FORMATS.EMAIL)
      .required()
  )

Works when I change it to:

  .prop(
    'email',
    FluentSchema()
      .asString()
      .format(FORMATS.EMAIL))
 .required()

All props should be required by default

Any app should be secure by default, Forget to make a field optional is not a security bug, but forget to make a field required is a security bug.

Any comments?

Unexpected typing error in typescript.

#20 ๐Ÿ› Bug Report

Using the the code below i ran into an unexpected error thrown from the typescript compiler.

To Reproduce

Steps to reproduce the behavior:

Paste your code here:

import * as S from 'fluent-schema'
import * as util from 'util'

const schema = S.object()
  .definition('Developer', S.object()
    .definition('env', S.object()
      .prop('account', S.string())
      .prop('region', S.string())
    )
  )
  .valueOf()

console.log(util.inspect(schema, false, null, true))

Expected behavior

I expect no errors to be reported back from the type script compiler.

Paste the results here:

example.ts:8:25 - error: TS2239: Propery 'string' does not exist on type 'typeof import("...node_modules/fluent-schema/src/FluentSchema")'.

Your Environment

  • node version: 12.16.1
  • os: Linux
  • tsc version:: 3.8.3

Compose schemas

Compose schemas

Extend JSON schemas isn't simple. A way is using allOf pointing to another schema

e.g.

const base = S.object().id('base').prop('foo', S.string())
const extended = S.object().id('extended').allOf(S.ref('base')).prop('bar')

This works fine.
Instead, if the base uses additionalProperties:false there is an issue.

const base = S.object().id('base').prop('foo', S.string()).additionalProperties(false) 
const extended = S.object().id('extended').allOf(S.ref('base'))prop('bar')

in that case, the validator will reject the object {foo: 'foo', bar: 'bar'} because bar is an extra property for the base schema.

Motivation

We could implement the composition via Fluent Schema.

const base = S.object()
  .additionalProperties(false)
  .prop('foo', S.string())

const extended = S.object(base)
  .prop('bar', S.number())

This will generate a unique schema which contains the rules from the base and the extended allowing additionalProperties to work correctly

Rename fluent-schema to fluent-json-schema

##. Rename fluent-schema to fluent-json-schema

Rename fluent-schema to fluent-json-schema

Motivation

To foster discoverability of the package via npmjs.com and search engines in general. I believe it would help to have JSON directly in the name aka fluent-json-schema

Example

npm install fluent-json-schema

Using custom keywords?

AJV has support to add custom keywords, but I don't see any way of using these with fluent schema.

AJV will accept this:

{
  foo: {
    type: 'integer'
  },
  myKeyword: true,
}

but I have no idea how to do something like this in fluent-schema, if its even possible.

There is no tags function

There is no tags function S.tags()

I don't know if it's added in the latest draft or it can be added in the current draft (7)

Motivation

I want to integrate it with fastify-swagger so I can make sections using those tags

Example

const userSchema = {
  body: S.tags('user')
     .object()
}

Differentiate schemas by type

it's a large refactoring in order to move toward 1.0

Create:

  • BaseSchema (id, title, example, required, etc.)
  • StringSchema (format, pattern, etc)
  • ArraySchema (items, etc)
  • ObjectSchema (properties, etc)
  • NumberSchema (min, max, etc)
  • IntegerSchema (min, max, etc)
  • BooleanSchema
  • Refactor Typescript types

New syntax

const schema = FluentSchema()
      .id('http://bar.com/object')
      .title('A object')
      .description('A object desc')
      .prop(
        'name',
        FluentSchema()
          .asString()
          .id('http://foo.com/string')
          .title('A string')
          .description('A string desc')
          .pattern(/.*/g)
          .format('date-time')
      )
      .valueOf()

it wont hint for wrong types like:

StringSchema().min(5) // Number validation keywords

or

FluentSchema().minLength(10) // String validation keywords

This allows:

  • better code hitting by type
  • smaller implementation files
  • Tests by type

not() remove all types

๐Ÿ› Bug Report

Hi, I'm trying this lib to put more "fluent" ๐Ÿ˜ examples on fastify's documentation.

Testing the not condition I found an unexpected behaviour during the generation of the schema with not() function, where the before one/any/all of lose the array of types.

I has also check the spec of not and could be a good idea to change the not API and add a schema as input instead of chain a anyOf, allOf or oneOf?

To Reproduce

const { FluentSchema } = require('fluent-schema')

const schema = FluentSchema()
  .prop('multipleRestrictedTypesKey')
  .oneOf([FluentSchema().asString(), FluentSchema().asNumber().minimum(10)])
  .prop('notTypeKey', FluentSchema().asString())
  .not()
  .oneOf([FluentSchema().asString().pattern('js$')])

console.log(JSON.stringify(schema.valueOf(), null, 2))

Without NOT (commenting .not())

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "multipleRestrictedTypesKey": {
      "oneOf": [
        {
          "type": "string"
        },
        {
          "type": "number",
          "minimum": 10
        }
      ]
    },
    "notTypeKey": {
      "oneOf": [
        {
          "type": "string",
          "pattern": "js$"
        }
      ]
    }
  }
}

With NOT

Note the multipleRestrictedTypesKey array of types is missing.

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "multipleRestrictedTypesKey": {
      "type": "string"
    },
    "notTypeKey": {
      "not": {
        "oneOf": [
          {
            "type": "string",
            "pattern": "*js$"
          }
        ]
      }
    }
  }
}

Your Environment

  • node version: 8.14.0
  • fluent-schema: 0.4.0
  • os: Windows

Unable to use allOf/anyOf/oneOf

๐Ÿ› Bug Report

If I use allOf/anyOf/oneOf as part of S.object() the final output drops "type": "object" from the schema resulting in errors such as: strict mode: missing type "object" for keyword "maxProperties" at "#/properties/definition" (strictTypes)

To Reproduce

Steps to reproduce the behavior:

S.object().prop(
  'definition',
  S.object()
    .maxProperties(100)
    .allOf([
      S.ifThen(
        S.object().prop('type', S.const('foo')),
        S.object()
          .prop('parameters', S.object().prop('bar', S.string()))
          .required()
      )
    ])
)

Expected behavior

A clear and concise description of what you expected to happen.

{
      "$schema": "http://json-schema.org/draft-07/schema#",
      "type": "object",
      "properties": {
        "definition": {
         "type": "object",   /// THIS IS MISSING FROM OUTPUT
          "maxProperties": 100,
          "allOf": [
            {
              "if": {
                "properties": {
                  "type": {
                    "const": "foo"
                  }
                }
              },
              "then": {
                "properties": {
                  "parameters": {
                    "type": "object",
                    "properties": {
                      "bar": {
                        "type": "string"
                      }
                    }
                  }
                },
                "required": [
                  "parameters"
                ]
              }
            }
          ]
        }
      }
    }

Your Environment

  • node version: 14
  • fluent-json-schema version: >=2.0.2
  • os: Mac

additionalProperties() unexpected behaviour depending on definition order

๐Ÿ› Bug Report

additionalProperties() changes behaviour depending on where it is positioned in the schema definition.
For example putting additionalProperties()after a prop() statement causes the prop to get the additionalProperties configuration instead of the root object.

To Reproduce

Steps to reproduce the behavior:

const S = require('.')
console.log(
  S.object()
  .prop('test', S.string())
  .additionalProperties(false)
  .valueOf()
)

Results in:

{
  '$schema': 'http://json-schema.org/draft-07/schema#',
  type: 'object',
  properties: { test: { additionalProperties: false, type: 'string' } }
}

While this:

const S = require('.')
console.log(
  S.object()
  .additionalProperties(false)
  .prop('test', S.string())
  .valueOf()
)

Results in:

{
  '$schema': 'http://json-schema.org/draft-07/schema#',
  type: 'object',
  additionalProperties: false,
  properties: { test: { type: 'string' } }
}

And that's the expected behaviour.

Expected behavior

There should be no differences between the two different scenarios described above, and the second one has the expected result.

Your Environment

  • node version: 14.15.1
  • fastify version: 3.9.2
  • os: Mac
  • any other relevant information: you're doing an amazing job!

Related issues/questions

fastify/help#368

Typings Intellisense only works if lib is import with .

๐Ÿ› Bug Report

The autocomplete from the typings only works if the library is imported with .default

To Reproduce

Steps to reproduce the behavior:

Paste your code here:

// This way autocompleted is working but the execution do not work.

const S = require('fluent-schema').default

S.object();

// This way autocompleted is not working but the execution works.

const S = require('fluent-schema')

S.object();

Expected behavior

On import fluent-schema without default autocomplete must be available

Paste the results here:

const S = require('fluent-schema')

S.object();

Your Environment

  • node version: 12
  • os: Mac
  • IDE: visual studio code

.id and .required not returning schema object

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure it has not already been reported

Fastify version

^3.4.1

Node.js version

14

Operating system

macOS

Operating system version (i.e. 20.04, 11.3, 10)

Big Sur 11.3.1

Description

While building schema and calling .id or .required, and after that trying to add some props, rises error:

TypeError: Schema.id(...).object is not a function

Steps to Reproduce

const Schema = require('fluent-json-schema')

Schema
.id('my-example-id')
.object()

Rise next error:
TypeError: Schema.id(...).object is not a function

The same error appears when trying to add some props after colling .required

Expected Behavior

Ability to make chain like this:

Schema.id('some-id').required(['param1', 'param2']).props('name', Schema.string())

anyOf, allOf, oneOf don't support an array of FluentSchema

it's currently failing

it('sets an array of FluentSchema', () => {
        expect(
          FluentSchema()
            .prop('prop')
            .allOf([FluentSchema().asString()])
            .valueOf()
        ).toEqual({
          $schema: 'http://json-schema.org/draft-07/schema#',
          properties: {
            prop: {
              allOf: [{ type: 'string' }],
            },
          },
          type: 'object',
        })
      })

Hack workaround for multiple-extend

๐Ÿ› Bug Report

multiple extend was broken here: #69
it was sort of fixed here: #74

however, the result is the hack function below to extend multiple times:

export const extendMultiple = (...schemas: ObjectSchema[]): ObjectSchema =>
  schemas.reduce((prev, curr) => curr.extend(prev), S.object());

To Reproduce

// extend is not a function (technically, the second extend doesn't exist)
S.object().extend(S.object()).extend(S.object())
// ...or
const schema = extendMultiple(S.object(), S.object(), S.object());

Expected behavior

extend shouldn't be excluded from the return value here: https://github.com/fastify/fluent-schema/blob/5ffffb8110a6fe845ca6cd7bbaaf2dc358960b5a/src/ObjectSchema.js#L261-L278

Your Environment

  • node version: 12
  • os: Mac,
  • fluent-schema version: 1.0.3

Question: how to implement a basic object schema?

Basically, I want to implement this test with fluent-schema -- https://github.com/fastify/fast-json-stringify/blob/06acd3bf09a6c0136eb442d0d95e2a5b1fd85cf2/test/additionalProperties.test.js#L68-L79

My attempt keeps resulting in various errors -- https://runkit.com/embed/ep1pfme4238t

var fastJson = require('fast-json-stringify')
var fluent = require("fluent-schema")
var schema = fluent.object().prop(
    'error',
    fluent.object().additionalProperties(true).prop('message')
)
console.log(schema.valueOf())
var stringify = fastJson(schema.valueOf())
stringify({ error: { message: 'foo', code: 'internal_server_error' } })

Get "some" properties from schema (easier than ref/id)

๐Ÿš€ Feature Proposal

v0.1 - just an idea, hoping to get some feedback.

Get Fluent Schema properties from another Fluent Schema to re-use in other schemas. This way you could build schemas easily from existing schemas.

I realize that with extend and $id, $ref, this is possible. However, I ended up with a schema.js file which had a lot of properties defined which I would then use all over the place. It would be nice to have one larger / more complex schema as "the source of truth" and be able to create sub schemas from it.

Motivation

I've created a fairly complex JsonSchema object using fluent-schema. Now I'd like to create some smaller schemas from it. One of the ways is described here https://www.fastify.io/docs/latest/Fluent-Schema/#reuse using ref/id. While this is the official JsonSchema way of doing so, I thought it could be simpler.

Example

// Create very long schema
const userSchema =  S.object()
  .prop('id', S.integer().readOnly())
  .prop('username', S.string().minLength(3).maxLength(48))
  .prop('password', S.string().minLength(8).maxLength(255))
  .prop(...);

// Now use some of those props somewhere else
const otherSchema = userSchema.only(['username', 'password'])

Additionally, instead of https://github.com/fastify/fluent-schema#extend-schema, one could do

const userSchema = S.object()
  .prop('id', S.string().format('uuid'))
  .prop('username', S.string())
  .prop('password', S.string())
  .prop('createdAt', S.string().format('time'))
  .prop('updatedAt', S.string().format('time'));

const userSchemaForPost = userSchema.only(['username', 'password']);

Allow using non-standard formats

The format method checks if the passed format is supported by the specification. However, validator can support custom, non-standard formats, for example Ajv has addFormat method. Would it be possible to allow passing non-standard format?

To maintain current behavior, perhaps it could be a different method, for example:

FluentSchema().asString().customFormat('my-custom-format')
  // => {type: 'string', format: 'my-custom-format'}

add S.raw

๐Ÿš€ Allow injecting a raw JSON directly in fluent-schema

Motivation

A few users asked:

  • add custom implementation related to AJV or other JSON Schema validation tools.
  • inherited schema from a JSON schema and not our wrapper

ref. #51 #16 #47

Examples

S.object()
  .prop('foo', S.integer())
  .prop('myKeyword', S.raw({true})
S.definition(S.raw({ foo: 'bar'}))
S.object().prop('foo').extend(S.raw('{"bar": { "type": "string"}}'))

In order to implement that we need to implement a parser to build the fluent-schema internal data structure based on the raw JSON.

https://github.com/fastify/fluent-schema/blob/master/src/ObjectSchema.js#L13

 {
  type: 'object',
  definitions: [],
  properties: [],
  required: [],
}

Nullable Prop with complex object fails when value is null

Hi there,
I'm not sure if this is a bug or a feature, but we have a property of a complext JSON type that can be null. And in the case that prop is null, the serializer throws an error:

TypeError: Cannot read property 'toJSON' of null
    at $mainroom (eval at build (C:\Dev\gjovanov\roomler\node_modules\fast-json-stringify\index.js:132:20), <anonymous>:184:30)
    at Object.$main (eval at build (C:\Dev\gjovanov\roomler\node_modules\fast-json-stringify\index.js:132:20), <anonymous>:119:17)
    at serialize (C:\Dev\gjovanov\roomler\node_modules\fastify\lib\validation.js:130:41)
    at preserializeHookEnd (C:\Dev\gjovanov\roomler\node_modules\fastify\lib\reply.js:263:15)
    at preserializeHook (C:\Dev\gjovanov\roomler\node_modules\fastify\lib\reply.js:250:5)

This is the model:

const room = S.object()
  .prop('_id', S.string())
  .prop('owner', S.string())
  .prop('name', S.string())
  .prop('moderators', S.array().items(S.string()))
  .prop('members', S.array().items(S.string()))

const invite = S.object()
  .prop('_id', S.string())
  .prop('inviter', S.string())
  .prop('room', room) // THROWS an error if property room has null value
  .prop('email', S.string())
  .prop('type', S.string())
  .prop('status', S.string().enum(statuses))
  .prop('createdAt', S.string())
  .prop('updatedAt', S.string())

Do you have any suggestion to support nullable props with complex JSON objects?
Thanks.

/GJ

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.