Repository: fastify/fluent-json-schema Branch: main Commit: 09a1e3e3ab56 Files: 44 Total size: 265.6 KB Directory structure: gitextract_nx7y_40v/ ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ ├── ci.yml │ └── lock-threads.yml ├── .gitignore ├── .husky/ │ └── pre-commit ├── .npmignore ├── .npmrc ├── .nvmrc ├── LICENSE ├── README.md ├── docs/ │ └── API.md ├── eslint.config.js ├── package.json ├── src/ │ ├── ArraySchema.js │ ├── ArraySchema.test.js │ ├── BaseSchema.js │ ├── BaseSchema.test.js │ ├── BooleanSchema.js │ ├── BooleanSchema.test.js │ ├── FluentJSONSchema.js │ ├── FluentSchema.integration.test.js │ ├── FluentSchema.test.js │ ├── IntegerSchema.js │ ├── IntegerSchema.test.js │ ├── MixedSchema.js │ ├── MixedSchema.test.js │ ├── NullSchema.js │ ├── NullSchema.test.js │ ├── NumberSchema.js │ ├── NumberSchema.test.js │ ├── ObjectSchema.js │ ├── ObjectSchema.test.js │ ├── RawSchema.js │ ├── RawSchema.test.js │ ├── StringSchema.js │ ├── StringSchema.test.js │ ├── example.js │ ├── schemas/ │ │ └── basic.json │ ├── utils.js │ └── utils.test.js └── types/ ├── FluentJSONSchema.d.ts └── FluentJSONSchema.test-d.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # http://editorconfig.org root = true [*] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.md] trim_trailing_whitespace = false ================================================ FILE: .gitattributes ================================================ # Set default behavior to automatically convert line endings * text=auto eol=lf ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "npm" directory: "/" schedule: interval: "monthly" open-pull-requests-limit: 10 ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: - main - next - 'v*' paths-ignore: - 'docs/**' - '*.md' pull_request: paths-ignore: - 'docs/**' - '*.md' # This allows a subsequently queued workflow run to interrupt previous runs concurrency: group: "${{ github.workflow }}-${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" cancel-in-progress: true permissions: contents: read jobs: test: permissions: contents: write pull-requests: write uses: fastify/workflows/.github/workflows/plugins-ci.yml@v6 with: lint: true license-check: true ================================================ FILE: .github/workflows/lock-threads.yml ================================================ name: Lock Threads on: schedule: - cron: '0 0 1 * *' workflow_dispatch: concurrency: group: lock permissions: contents: read jobs: lock-threads: permissions: issues: write pull-requests: write uses: fastify/workflows/.github/workflows/lock-threads.yml@v6 ================================================ FILE: .gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* .pnpm-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) web_modules/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional stylelint cache .stylelintcache # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variable files .env .env.development.local .env.test.local .env.production.local .env.local # parcel-bundler cache (https://parceljs.org/) .cache .parcel-cache # Next.js build output .next out # Nuxt.js build / generate output .nuxt dist # Gatsby files .cache/ # Comment in the public line in if your project uses Gatsby and not Next.js # https://nextjs.org/blog/next-9-1#public-directory-support # public # vuepress build output .vuepress/dist # vuepress v2.x temp and cache directory .temp .cache # Docusaurus cache and generated files .docusaurus # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # TernJS port file .tern-port # Stores VSCode versions used for testing VSCode extensions .vscode-test # yarn v2 .yarn/cache .yarn/unplugged .yarn/build-state.yml .yarn/install-state.gz .pnp.* # Vim swap files *.swp # macOS files .DS_Store # Clinic .clinic # lock files bun.lockb package-lock.json pnpm-lock.yaml yarn.lock # editor files .vscode .idea #tap files .tap/ ================================================ FILE: .husky/pre-commit ================================================ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" npx lint-staged ================================================ FILE: .npmignore ================================================ .editorconfig .gitignore .husky .npmignore .nvmrc .prettierrc .travis.yml yarn.lock .idea coverage ================================================ FILE: .npmrc ================================================ ignore-scripts=true package-lock=false ================================================ FILE: .nvmrc ================================================ v14.19.0 ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018-present The Fastify team Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # fluent-json-schema A fluent API to generate JSON schemas (draft-07) for Node.js and browser. Framework agnostic. [![view on npm](https://img.shields.io/npm/v/fluent-json-schema.svg)](https://www.npmjs.org/package/fluent-json-schema) [![CI](https://github.com/fastify/fluent-json-schema/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/fastify/fluent-json-schema/actions/workflows/ci.yml) [![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard) ## Features - Fluent schema implements JSON Schema draft-07 standards - Faster and shorter way to write a JSON Schema via a [fluent API](https://en.wikipedia.org/wiki/Fluent_interface) - 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 100% ## Install npm i fluent-json-schema or yarn add fluent-json-schema ## Usage ```javascript 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: ```json { "$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`: ```typescript 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`: ```typescript 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: ```typescript 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 are a few examples using [AJV](https://ajv.js.org/): npm i ajv or yarn add ajv ### Validate an empty model Snippet: ```javascript 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: ```javascript 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: ```javascript user = { email: 'test@foo.com', 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: ```javascript user = { email: 'test@foo.com', 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 evaluates 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. ```js 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. ```js 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. ```js 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 understand the Fluent Schema API and improve the user experience of your tool. ```js const S = require('fluent-json-schema') const schema = S.object().prop('foo', S.string()).prop('bar', S.number()) console.log(schema.isFluentSchema) // true ``` ## Documentation - [Full API Documentation](./docs/API.md). - [JSON schema draft-07 reference](https://json-schema.org/draft-07/draft-handrews-json-schema-01). ## Acknowledgments Thanks to [Matteo Collina](https://twitter.com/matteocollina) for pushing me to implement this utility! 🙏 ## Related projects - JSON Schema [Draft 7](http://json-schema.org/specification-links.html#draft-7) - [Understanding JSON Schema](https://json-schema.org/understanding-json-schema/) (despite referring to draft 6 the guide is still good for grasping the main concepts) - [AJV](https://ajv.js.org/) JSON Schema validator - [jsonschema.net](https://www.jsonschema.net/) an online JSON Schema visual editor (it does not support advanced features) ## License Licensed under [MIT](./LICENSE). ================================================ FILE: docs/API.md ================================================ ## Functions
ArraySchema([options])ArraySchema

Represents a ArraySchema.

items(items)FluentSchema

This keyword determines how child instances validate for arrays, and does not directly validate the immediate instance itself. If "items" is a schema, validation succeeds if all elements in the array successfully validate against that schema. If "items" is an array of schemas, validation succeeds if each element of the instance validates against the schema at the same position, if any. Omitting this keyword has the same behavior as an empty schema.

reference

additionalItems(items)FluentSchema

This keyword determines how child instances validate for arrays, and does not directly validate the immediate instance itself.

reference

contains(value)FluentSchema

An array instance is valid against "contains" if at least one of its elements is valid against the given schema.

reference

uniqueItems(boolean)FluentSchema

If this keyword has boolean value false, the instance validates successfully. If it has boolean value true, the instance validates successfully if all of its elements are unique. Omitting this keyword has the same behavior as a value of false.

reference

minItems(min)FluentSchema

An array instance is valid against "minItems" if its size is greater than, or equal to, the value of this keyword. Omitting this keyword has the same behavior as a value of 0.

reference

maxItems(max)FluentSchema

An array instance is valid against "minItems" if its size is greater than, or equal to, the value of this keyword. Omitting this keyword has the same behavior as a value of 0.

reference

BaseSchema([options])BaseSchema

Represents a BaseSchema.

id(id)BaseSchema

It defines a URI for the schema, and the base URI that other URI references within the schema are resolved against.

reference

title(title)BaseSchema

It can be used to decorate a user interface with information about the data produced by this user interface. A title will preferably be short.

reference

description(description)BaseSchema

It can be used to decorate a user interface with information about the data produced by this user interface. A description provides explanation about the purpose of the instance described by the schema.

reference

examples(examples)BaseSchema

The value of this keyword MUST be an array. There are no restrictions placed on the values within the array.

reference

ref(ref)BaseSchema

The value must be a valid id e.g. #properties/foo

enum(values)BaseSchema

The value of this keyword MUST be an array. This array SHOULD have at least one element. Elements in the array SHOULD be unique.

reference

const(value)BaseSchema

The value of this keyword MAY be of any type, including null.

reference

default(defaults)BaseSchema

There are no restrictions placed on the value of this keyword.

reference

readOnly(isReadOnly)BaseSchema

The value of readOnly can be left empty to indicate the property is readOnly. It takes an optional boolean which can be used to explicitly set readOnly true/false.

reference

writeOnly(isWriteOnly)BaseSchema

The value of writeOnly can be left empty to indicate the property is writeOnly. It takes an optional boolean which can be used to explicitly set writeOnly true/false.

reference

deprecated(isDeprecated)BaseSchema

The value of deprecated can be left empty to indicate the property is deprecated. It takes an optional boolean which can be used to explicitly set deprecated true/false.

reference

required()FluentSchema

Required has to be chained to a property: Examples:

  • S.prop('prop').required()
  • S.prop('prop', S.number()).required()
  • S.required(['foo', 'bar'])

reference

not(not)BaseSchema

This keyword's value MUST be a valid JSON Schema. An instance is valid against this keyword if it fails to validate successfully against the schema defined by this keyword.

reference

anyOf(schemas)BaseSchema

It MUST be a non-empty array. Each item of the array MUST be a valid JSON Schema.

reference

allOf(schemas)BaseSchema

It MUST be a non-empty array. Each item of the array MUST be a valid JSON Schema.

reference

oneOf(schemas)BaseSchema

It MUST be a non-empty array. Each item of the array MUST be a valid JSON Schema.

reference

ifThen(ifClause, thenClause)BaseSchema

This validation outcome of this keyword's subschema has no direct effect on the overall validation result. Rather, it controls which of the "then" or "else" keywords are evaluated. When "if" is present, and the instance successfully validates against its subschema, then validation succeeds against this keyword if the instance also successfully validates against this keyword's subschema.

ifThenElse(ifClause, thenClause, elseClause)BaseSchema

When "if" is present, and the instance fails to validate against its subschema, then validation succeeds against this keyword if the instance successfully validates against this keyword's subschema.

raw(fragment)BaseSchema

Because the differences between JSON Schemas and Open API (Swagger) it can be handy to arbitrary modify the schema injecting a fragment

  • Examples:
  • S.number().raw({ nullable:true })
  • S.string().format('date').raw({ formatMaximum: '2020-01-01' })
valueOf([options])object

It returns all the schema values

BooleanSchema([options])StringSchema

Represents a BooleanSchema.

S([options])S

Represents a S.

string()StringSchema

Set a property to type string

reference

number()NumberSchema

Set a property to type number

reference

integer()IntegerSchema

Set a property to type integer

reference

boolean()BooleanSchema

Set a property to type boolean

reference

array()ArraySchema

Set a property to type array

reference

object()ObjectSchema

Set a property to type object

reference

null()NullSchema

Set a property to type null

reference

mixed(types)MixedSchema

A mixed schema is the union of multiple types (e.g. ['string', 'integer']

raw(fragment)BaseSchema

Because the differences between JSON Schemas and Open API (Swagger) it can be handy to arbitrary modify the schema injecting a fragment

  • Examples:
  • S.raw({ nullable:true, format: 'date', formatMaximum: '2020-01-01' })
  • S.string().format('date').raw({ formatMaximum: '2020-01-01' })
IntegerSchema([options])NumberSchema

Represents a NumberSchema.

MixedSchema([options])StringSchema

Represents a MixedSchema.

NullSchema([options])StringSchema

Represents a NullSchema.

null()FluentSchema

Set a property to type null

reference

NumberSchema([options])NumberSchema

Represents a NumberSchema.

minimum(min)FluentSchema

It represents an inclusive lower limit for a numeric instance.

reference

exclusiveMinimum(min)FluentSchema

It represents an exclusive lower limit for a numeric instance.

reference

maximum(max)FluentSchema

It represents an inclusive upper limit for a numeric instance.

reference

exclusiveMaximum(max)FluentSchema

It represents an exclusive upper limit for a numeric instance.

reference

multipleOf(multiple)FluentSchema

It's strictly greater than 0.

reference

ObjectSchema([options])StringSchema

Represents a ObjectSchema.

id(id)

It defines a URI for the schema, and the base URI that other URI references within the schema are resolved against. Calling id on an ObjectSchema will alway set the id on the root of the object rather than in its "properties", which differs from other schema types.

reference

additionalProperties(value)FluentSchema

This keyword determines how child instances validate for objects, and does not directly validate the immediate instance itself. Validation with "additionalProperties" applies only to the child values of instance names that do not match any names in "properties", and do not match any regular expression in "patternProperties". For all such properties, validation succeeds if the child instance validates against the "additionalProperties" schema. Omitting this keyword has the same behavior as an empty schema.

reference

maxProperties(max)FluentSchema

An object instance is valid against "maxProperties" if its number of properties is less than, or equal to, the value of this keyword.

reference

minProperties(min)FluentSchema

An object instance is valid against "minProperties" if its number of properties is greater than, or equal to, the value of this keyword.

reference

patternProperties(opts)FluentSchema

Each property name of this object SHOULD be a valid regular expression, according to the ECMA 262 regular expression dialect. Each property value of this object MUST be a valid JSON Schema. This keyword determines how child instances validate for objects, and does not directly validate the immediate instance itself. Validation of the primitive instance type against this keyword always succeeds. Validation succeeds if, for each instance name that matches any regular expressions that appear as a property name in this keyword's value, the child instance for that name successfully validates against each schema that corresponds to a matching regular expression.

reference

dependencies(opts)FluentSchema

This keyword specifies rules that are evaluated if the instance is an object and contains a certain property. This keyword's value MUST be an object. Each property specifies a dependency. Each dependency value MUST be an array or a valid JSON Schema. If the dependency value is a subschema, and the dependency key is a property in the instance, the entire instance must validate against the dependency value. If the dependency value is an array, each element in the array, if any, MUST be a string, and MUST be unique. If the dependency key is a property in the instance, each of the items in the dependency value must be a property that exists in the instance.

reference

dependentRequired(opts)FluentSchema

The value of "properties" MUST be an object. Each dependency value MUST be an array. Each element in the array MUST be a string and MUST be unique. If the dependency key is a property in the instance, each of the items in the dependency value must be a property that exists in the instance.

reference

dependentSchemas(opts)FluentSchema

The value of "properties" MUST be an object. The dependency value MUST be a valid JSON Schema. Each dependency key is a property in the instance and the entire instance must validate against the dependency value.

reference

propertyNames(value)FluentSchema

If the instance is an object, this keyword validates if every property name in the instance validates against the provided schema. Note the property name that the schema is testing will always be a string.

reference

prop(name, props)FluentSchema

The value of "properties" MUST be an object. Each value of this object MUST be a valid JSON Schema.

reference

only(properties)ObjectSchema

Returns an object schema with only a subset of keys provided. If called on an ObjectSchema with an $id, it will be removed and the return value will be considered a new schema.

without(properties)ObjectSchema

Returns an object schema without a subset of keys provided. If called on an ObjectSchema with an $id, it will be removed and the return value will be considered a new schema.

definition(name, props)FluentSchema

The "definitions" keywords provides a standardized location for schema authors to inline re-usable JSON Schemas into a more general schema. There are no restrictions placed on the values within the array.

reference

RawSchema(schema)FluentSchema

Represents a raw JSON Schema that will be parsed

StringSchema([options])StringSchema

Represents a StringSchema.

minLength(min)StringSchema

A string instance is valid against this keyword if its length is greater than, or equal to, the value of this keyword. The length of a string instance is defined as the number of its characters as defined by RFC 7159 [RFC7159].

reference

maxLength(max)StringSchema

A string instance is valid against this keyword if its length is less than, or equal to, the value of this keyword. The length of a string instance is defined as the number of its characters as defined by RFC 7159 [RFC7159].

reference

format(format)StringSchema

A string value can be RELATIVE_JSON_POINTER, JSON_POINTER, UUID, REGEX, IPV6, IPV4, HOSTNAME, EMAIL, URL, URI_TEMPLATE, URI_REFERENCE, URI, TIME, DATE, DATE_TIME, ISO_TIME, ISO_DATE_TIME.

reference

pattern(pattern)StringSchema

This string SHOULD be a valid regular expression, according to the ECMA 262 regular expression dialect. A string instance is considered valid if the regular expression matches the instance successfully.

reference

contentEncoding(encoding)StringSchema

If the instance value is a string, this property defines that the string SHOULD be interpreted as binary data and decoded using the encoding named by this property. RFC 2045, Sec 6.1 [RFC2045] lists the possible values for this property.

reference

contentMediaType(mediaType)StringSchema

The value of this property must be a media type, as defined by RFC 2046 [RFC2046]. This property defines the media type of instances which this schema defines.

reference

## ArraySchema([options]) ⇒ [ArraySchema](#ArraySchema) Represents a ArraySchema. **Kind**: global function | Param | Type | Default | Description | | --- | --- | --- | --- | | [options] | Object | | Options | | [options.schema] | [StringSchema](#StringSchema) | | Default schema | | [options.generateIds] | [boolean](#boolean) | false | generate the id automatically e.g. #properties.foo | ## items(items) ⇒ FluentSchema This keyword determines how child instances validate for arrays, and does not directly validate the immediate instance itself. If "items" is a schema, validation succeeds if all elements in the array successfully validate against that schema. If "items" is an array of schemas, validation succeeds if each element of the instance validates against the schema at the same position, if any. Omitting this keyword has the same behavior as an empty schema. [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.4.1) **Kind**: global function | Param | Type | | --- | --- | | items | FluentSchema \| Array.<FluentSchema> | ## additionalItems(items) ⇒ FluentSchema This keyword determines how child instances validate for arrays, and does not directly validate the immediate instance itself. [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.4.2) **Kind**: global function | Param | Type | | --- | --- | | items | FluentSchema \| [boolean](#boolean) | ## contains(value) ⇒ FluentSchema An array instance is valid against "contains" if at least one of its elements is valid against the given schema. [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.4.6) **Kind**: global function | Param | Type | | --- | --- | | value | FluentSchema | ## uniqueItems(boolean) ⇒ FluentSchema If this keyword has boolean value false, the instance validates successfully. If it has boolean value true, the instance validates successfully if all of its elements are unique. Omitting this keyword has the same behavior as a value of false. [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.4.5) **Kind**: global function | Param | Type | | --- | --- | | boolean | [boolean](#boolean) | ## minItems(min) ⇒ FluentSchema An array instance is valid against "minItems" if its size is greater than, or equal to, the value of this keyword. Omitting this keyword has the same behavior as a value of 0. [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.4.4) **Kind**: global function | Param | Type | | --- | --- | | min | [number](#number) | ## maxItems(max) ⇒ FluentSchema An array instance is valid against "minItems" if its size is greater than, or equal to, the value of this keyword. Omitting this keyword has the same behavior as a value of 0. [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.4.3) **Kind**: global function | Param | Type | | --- | --- | | max | [number](#number) | ## BaseSchema([options]) ⇒ [BaseSchema](#BaseSchema) Represents a BaseSchema. **Kind**: global function | Param | Type | Default | Description | | --- | --- | --- | --- | | [options] | Object | | Options | | [options.schema] | [BaseSchema](#BaseSchema) | | Default schema | | [options.generateIds] | [boolean](#boolean) | false | generate the id automatically e.g. #properties.foo | ## id(id) ⇒ [BaseSchema](#BaseSchema) It defines a URI for the schema, and the base URI that other URI references within the schema are resolved against. [reference](https://tools.ietf.org/html/draft-handrews-json-schema-01#section-8.2) **Kind**: global function | Param | Type | Description | | --- | --- | --- | | id | [string](#string) | an #id | ## title(title) ⇒ [BaseSchema](#BaseSchema) It can be used to decorate a user interface with information about the data produced by this user interface. A title will preferably be short. [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.10.1) **Kind**: global function | Param | Type | | --- | --- | | title | [string](#string) | ## description(description) ⇒ [BaseSchema](#BaseSchema) It can be used to decorate a user interface with information about the data produced by this user interface. A description provides explanation about the purpose of the instance described by the schema. [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.10.1) **Kind**: global function | Param | Type | | --- | --- | | description | [string](#string) | ## examples(examples) ⇒ [BaseSchema](#BaseSchema) The value of this keyword MUST be an array. There are no restrictions placed on the values within the array. [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.10.4) **Kind**: global function | Param | Type | | --- | --- | | examples | [string](#string) | ## ref(ref) ⇒ [BaseSchema](#BaseSchema) The value must be a valid id e.g. #properties/foo **Kind**: global function | Param | Type | | --- | --- | | ref | [string](#string) | ## enum(values) ⇒ [BaseSchema](#BaseSchema) The value of this keyword MUST be an array. This array SHOULD have at least one element. Elements in the array SHOULD be unique. [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.1.2) **Kind**: global function | Param | Type | | --- | --- | | values | [array](#array) | ## const(value) ⇒ [BaseSchema](#BaseSchema) The value of this keyword MAY be of any type, including null. [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.1.3) **Kind**: global function | Param | | --- | | value | ## default(defaults) ⇒ [BaseSchema](#BaseSchema) There are no restrictions placed on the value of this keyword. [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.10.2) **Kind**: global function | Param | | --- | | defaults | ## readOnly(isReadOnly) ⇒ [BaseSchema](#BaseSchema) The value of readOnly can be left empty to indicate the property is readOnly. It takes an optional boolean which can be used to explicitly set readOnly true/false. [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.10.3) **Kind**: global function | Param | Type | | --- | --- | | isReadOnly | [boolean](#boolean) \| undefined | ## writeOnly(isWriteOnly) ⇒ [BaseSchema](#BaseSchema) The value of writeOnly can be left empty to indicate the property is writeOnly. It takes an optional boolean which can be used to explicitly set writeOnly true/false. [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.10.3) **Kind**: global function | Param | Type | | --- | --- | | isWriteOnly | [boolean](#boolean) \| undefined | ## deprecated(isDeprecated) ⇒ [BaseSchema](#BaseSchema) The value of deprecated can be left empty to indicate the property is deprecated. It takes an optional boolean which can be used to explicitly set deprecated true/false. [reference](https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.9.3) **Kind**: global function | Param | Type | | --- | --- | | isDeprecated | Boolean | ## required() ⇒ FluentSchema Required has to be chained to a property: Examples: - S.prop('prop').required() - S.prop('prop', S.number()).required() - S.required(['foo', 'bar']) [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.5.3) **Kind**: global function ## not(not) ⇒ [BaseSchema](#BaseSchema) This keyword's value MUST be a valid JSON Schema. An instance is valid against this keyword if it fails to validate successfully against the schema defined by this keyword. [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.7.4) **Kind**: global function | Param | Type | | --- | --- | | not | FluentSchema | ## anyOf(schemas) ⇒ [BaseSchema](#BaseSchema) It MUST be a non-empty array. Each item of the array MUST be a valid JSON Schema. [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.7.2) **Kind**: global function | Param | Type | | --- | --- | | schemas | [array](#array) | ## allOf(schemas) ⇒ [BaseSchema](#BaseSchema) It MUST be a non-empty array. Each item of the array MUST be a valid JSON Schema. [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.7.1) **Kind**: global function | Param | Type | | --- | --- | | schemas | [array](#array) | ## oneOf(schemas) ⇒ [BaseSchema](#BaseSchema) It MUST be a non-empty array. Each item of the array MUST be a valid JSON Schema. [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.7.3) **Kind**: global function | Param | Type | | --- | --- | | schemas | [array](#array) | ## ifThen(ifClause, thenClause) ⇒ [BaseSchema](#BaseSchema) This validation outcome of this keyword's subschema has no direct effect on the overall validation result. Rather, it controls which of the "then" or "else" keywords are evaluated. When "if" is present, and the instance successfully validates against its subschema, then validation succeeds against this keyword if the instance also successfully validates against this keyword's subschema. **Kind**: global function | Param | Type | Description | | --- | --- | --- | | ifClause | [BaseSchema](#BaseSchema) | [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.6.1) | | thenClause | [BaseSchema](#BaseSchema) | [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.6.2) | ## ifThenElse(ifClause, thenClause, elseClause) ⇒ [BaseSchema](#BaseSchema) When "if" is present, and the instance fails to validate against its subschema, then validation succeeds against this keyword if the instance successfully validates against this keyword's subschema. **Kind**: global function | Param | Type | Description | | --- | --- | --- | | ifClause | [BaseSchema](#BaseSchema) | [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.6.1) | | thenClause | [BaseSchema](#BaseSchema) | [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.6.2) | | elseClause | [BaseSchema](#BaseSchema) | [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.6.3) | ## raw(fragment) ⇒ [BaseSchema](#BaseSchema) Because the differences between JSON Schemas and Open API (Swagger) it can be handy to arbitrary modify the schema injecting a fragment * Examples: - S.number().raw({ nullable:true }) - S.string().format('date').raw({ formatMaximum: '2020-01-01' }) **Kind**: global function | Param | Type | Description | | --- | --- | --- | | fragment | [string](#string) | an arbitrary JSON Schema to inject [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.3.3) | ## valueOf([options]) ⇒ [object](#object) It returns all the schema values **Kind**: global function | Param | Type | Default | Description | | --- | --- | --- | --- | | [options] | Object | | Options | | [options.isRoot] | [boolean](#boolean) | true | Is a root level schema | ## BooleanSchema([options]) ⇒ [StringSchema](#StringSchema) Represents a BooleanSchema. **Kind**: global function | Param | Type | Default | Description | | --- | --- | --- | --- | | [options] | Object | | Options | | [options.schema] | [StringSchema](#StringSchema) | | Default schema | | [options.generateIds] | [boolean](#boolean) | false | generate the id automatically e.g. #properties.foo | ## S([options]) ⇒ [S](#S) Represents a S. **Kind**: global function | Param | Type | Default | Description | | --- | --- | --- | --- | | [options] | Object | | Options | | [options.schema] | [S](#S) | | Default schema | | [options.generateIds] | [boolean](#boolean) | false | generate the id automatically e.g. #properties.foo | ## string() ⇒ [StringSchema](#StringSchema) Set a property to type string [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.3) **Kind**: global function ## number() ⇒ [NumberSchema](#NumberSchema) Set a property to type number [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#numeric) **Kind**: global function ## integer() ⇒ [IntegerSchema](#IntegerSchema) Set a property to type integer [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#numeric) **Kind**: global function ## boolean() ⇒ [BooleanSchema](#BooleanSchema) Set a property to type boolean [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.7) **Kind**: global function ## array() ⇒ [ArraySchema](#ArraySchema) Set a property to type array [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.4) **Kind**: global function ## object() ⇒ [ObjectSchema](#ObjectSchema) Set a property to type object [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.5) **Kind**: global function ## null() ⇒ [NullSchema](#NullSchema) Set a property to type null [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#general) **Kind**: global function ## mixed(types) ⇒ [MixedSchema](#MixedSchema) A mixed schema is the union of multiple types (e.g. ['string', 'integer'] **Kind**: global function | Param | Type | | --- | --- | | types | [Array.<string>](#string) | ## raw(fragment) ⇒ [BaseSchema](#BaseSchema) Because the differences between JSON Schemas and Open API (Swagger) it can be handy to arbitrary modify the schema injecting a fragment * Examples: - S.raw({ nullable:true, format: 'date', formatMaximum: '2020-01-01' }) - S.string().format('date').raw({ formatMaximum: '2020-01-01' }) **Kind**: global function | Param | Type | Description | | --- | --- | --- | | fragment | [string](#string) | an arbitrary JSON Schema to inject | ## IntegerSchema([options]) ⇒ [NumberSchema](#NumberSchema) Represents a NumberSchema. **Kind**: global function | Param | Type | Default | Description | | --- | --- | --- | --- | | [options] | Object | | Options | | [options.schema] | [NumberSchema](#NumberSchema) | | Default schema | | [options.generateIds] | [boolean](#boolean) | false | generate the id automatically e.g. #properties.foo | ## MixedSchema([options]) ⇒ [StringSchema](#StringSchema) Represents a MixedSchema. **Kind**: global function | Param | Type | Default | Description | | --- | --- | --- | --- | | [options] | Object | | Options | | [options.schema] | [MixedSchema](#MixedSchema) | | Default schema | | [options.generateIds] | [boolean](#boolean) | false | generate the id automatically e.g. #properties.foo | ## NullSchema([options]) ⇒ [StringSchema](#StringSchema) Represents a NullSchema. **Kind**: global function | Param | Type | Default | Description | | --- | --- | --- | --- | | [options] | Object | | Options | | [options.schema] | [StringSchema](#StringSchema) | | Default schema | | [options.generateIds] | [boolean](#boolean) | false | generate the id automatically e.g. #properties.foo | ## null() ⇒ FluentSchema Set a property to type null [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.1.1) **Kind**: global function ## NumberSchema([options]) ⇒ [NumberSchema](#NumberSchema) Represents a NumberSchema. **Kind**: global function | Param | Type | Default | Description | | --- | --- | --- | --- | | [options] | Object | | Options | | [options.schema] | [NumberSchema](#NumberSchema) | | Default schema | | [options.generateIds] | [boolean](#boolean) | false | generate the id automatically e.g. #properties.foo | ## minimum(min) ⇒ FluentSchema It represents an inclusive lower limit for a numeric instance. [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.2.4) **Kind**: global function | Param | Type | | --- | --- | | min | [number](#number) | ## exclusiveMinimum(min) ⇒ FluentSchema It represents an exclusive lower limit for a numeric instance. [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.2.5) **Kind**: global function | Param | Type | | --- | --- | | min | [number](#number) | ## maximum(max) ⇒ FluentSchema It represents an inclusive upper limit for a numeric instance. [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.2.2) **Kind**: global function | Param | Type | | --- | --- | | max | [number](#number) | ## exclusiveMaximum(max) ⇒ FluentSchema It represents an exclusive upper limit for a numeric instance. [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.2.3) **Kind**: global function | Param | Type | | --- | --- | | max | [number](#number) | ## multipleOf(multiple) ⇒ FluentSchema It's strictly greater than 0. [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.2.1) **Kind**: global function | Param | Type | | --- | --- | | multiple | [number](#number) | ## ObjectSchema([options]) ⇒ [StringSchema](#StringSchema) Represents a ObjectSchema. **Kind**: global function | Param | Type | Default | Description | | --- | --- | --- | --- | | [options] | Object | | Options | | [options.schema] | [StringSchema](#StringSchema) | | Default schema | | [options.generateIds] | [boolean](#boolean) | false | generate the id automatically e.g. #properties.foo | ## id(id) It defines a URI for the schema, and the base URI that other URI references within the schema are resolved against. Calling `id` on an ObjectSchema will alway set the id on the root of the object rather than in its "properties", which differs from other schema types. [reference](https://tools.ietf.org/html/draft-handrews-json-schema-01#section-8.2) **Kind**: global function | Param | Type | Description | | --- | --- | --- | | id | [string](#string) | an #id | ## additionalProperties(value) ⇒ FluentSchema This keyword determines how child instances validate for objects, and does not directly validate the immediate instance itself. Validation with "additionalProperties" applies only to the child values of instance names that do not match any names in "properties", and do not match any regular expression in "patternProperties". For all such properties, validation succeeds if the child instance validates against the "additionalProperties" schema. Omitting this keyword has the same behavior as an empty schema. [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.5.6) **Kind**: global function | Param | Type | | --- | --- | | value | FluentSchema \| [boolean](#boolean) | ## maxProperties(max) ⇒ FluentSchema An object instance is valid against "maxProperties" if its number of properties is less than, or equal to, the value of this keyword. [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.5.1) **Kind**: global function | Param | Type | | --- | --- | | max | [number](#number) | ## minProperties(min) ⇒ FluentSchema An object instance is valid against "minProperties" if its number of properties is greater than, or equal to, the value of this keyword. [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.5.2) **Kind**: global function | Param | Type | | --- | --- | | min | [number](#number) | ## patternProperties(opts) ⇒ FluentSchema Each property name of this object SHOULD be a valid regular expression, according to the ECMA 262 regular expression dialect. Each property value of this object MUST be a valid JSON Schema. This keyword determines how child instances validate for objects, and does not directly validate the immediate instance itself. Validation of the primitive instance type against this keyword always succeeds. Validation succeeds if, for each instance name that matches any regular expressions that appear as a property name in this keyword's value, the child instance for that name successfully validates against each schema that corresponds to a matching regular expression. [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.5.5) **Kind**: global function | Param | Type | | --- | --- | | opts | [object](#object) | ## dependencies(opts) ⇒ FluentSchema This keyword specifies rules that are evaluated if the instance is an object and contains a certain property. This keyword's value MUST be an object. Each property specifies a dependency. Each dependency value MUST be an array or a valid JSON Schema. If the dependency value is a subschema, and the dependency key is a property in the instance, the entire instance must validate against the dependency value. If the dependency value is an array, each element in the array, if any, MUST be a string, and MUST be unique. If the dependency key is a property in the instance, each of the items in the dependency value must be a property that exists in the instance. [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.5.7) **Kind**: global function | Param | Type | | --- | --- | | opts | [object](#object) | ## dependentRequired(opts) ⇒ FluentSchema The value of "properties" MUST be an object. Each dependency value MUST be an array. Each element in the array MUST be a string and MUST be unique. If the dependency key is a property in the instance, each of the items in the dependency value must be a property that exists in the instance. [reference](https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.6.5.4) **Kind**: global function | Param | Type | | --- | --- | | opts | [object](#object) | ## dependentSchemas(opts) ⇒ FluentSchema The value of "properties" MUST be an object. The dependency value MUST be a valid JSON Schema. Each dependency key is a property in the instance and the entire instance must validate against the dependency value. [reference](https://json-schema.org/draft/2019-09/json-schema-core.html#rfc.section.9.2.2.4) **Kind**: global function | Param | Type | | --- | --- | | opts | [object](#object) | ## propertyNames(value) ⇒ FluentSchema If the instance is an object, this keyword validates if every property name in the instance validates against the provided schema. Note the property name that the schema is testing will always be a string. [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.5.8) **Kind**: global function | Param | Type | | --- | --- | | value | FluentSchema | ## prop(name, props) ⇒ FluentSchema The value of "properties" MUST be an object. Each value of this object MUST be a valid JSON Schema. [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.5.4) **Kind**: global function | Param | Type | | --- | --- | | name | [string](#string) | | props | FluentSchema | ## only(properties) ⇒ [ObjectSchema](#ObjectSchema) Returns an object schema with only a subset of keys provided. If called on an ObjectSchema with an `$id`, it will be removed and the return value will be considered a new schema. **Kind**: global function | Param | Description | | --- | --- | | properties | a list of properties you want to keep | ## without(properties) ⇒ [ObjectSchema](#ObjectSchema) Returns an object schema without a subset of keys provided. If called on an ObjectSchema with an `$id`, it will be removed and the return value will be considered a new schema. **Kind**: global function | Param | Description | | --- | --- | | properties | a list of properties you dont want to keep | ## definition(name, props) ⇒ FluentSchema The "definitions" keywords provides a standardized location for schema authors to inline re-usable JSON Schemas into a more general schema. There are no restrictions placed on the values within the array. [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.9) **Kind**: global function | Param | Type | | --- | --- | | name | [string](#string) | | props | FluentSchema | ## RawSchema(schema) ⇒ FluentSchema Represents a raw JSON Schema that will be parsed **Kind**: global function | Param | Type | | --- | --- | | schema | Object | ## StringSchema([options]) ⇒ [StringSchema](#StringSchema) Represents a StringSchema. **Kind**: global function | Param | Type | Default | Description | | --- | --- | --- | --- | | [options] | Object | | Options | | [options.schema] | [StringSchema](#StringSchema) | | Default schema | | [options.generateIds] | [boolean](#boolean) | false | generate the id automatically e.g. #properties.foo | ## minLength(min) ⇒ [StringSchema](#StringSchema) A string instance is valid against this keyword if its length is greater than, or equal to, the value of this keyword. The length of a string instance is defined as the number of its characters as defined by RFC 7159 [RFC7159]. [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.3.2) **Kind**: global function | Param | Type | | --- | --- | | min | [number](#number) | ## maxLength(max) ⇒ [StringSchema](#StringSchema) A string instance is valid against this keyword if its length is less than, or equal to, the value of this keyword. The length of a string instance is defined as the number of its characters as defined by RFC 7159 [RFC7159]. [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.3.1) **Kind**: global function | Param | Type | | --- | --- | | max | [number](#number) | ## format(format) ⇒ [StringSchema](#StringSchema) A string value can be RELATIVE_JSON_POINTER, JSON_POINTER, UUID, REGEX, IPV6, IPV4, HOSTNAME, EMAIL, URL, URI_TEMPLATE, URI_REFERENCE, URI, TIME, DATE, DATE_TIME, ISO_TIME, ISO_DATE_TIME. [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.7.3) **Kind**: global function | Param | Type | | --- | --- | | format | [string](#string) | ## pattern(pattern) ⇒ [StringSchema](#StringSchema) This string SHOULD be a valid regular expression, according to the ECMA 262 regular expression dialect. A string instance is considered valid if the regular expression matches the instance successfully. [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.3.3) **Kind**: global function | Param | Type | | --- | --- | | pattern | [string](#string) | ## contentEncoding(encoding) ⇒ [StringSchema](#StringSchema) If the instance value is a string, this property defines that the string SHOULD be interpreted as binary data and decoded using the encoding named by this property. RFC 2045, Sec 6.1 [RFC2045] lists the possible values for this property. [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.8.3) **Kind**: global function | Param | Type | | --- | --- | | encoding | [string](#string) | ## contentMediaType(mediaType) ⇒ [StringSchema](#StringSchema) The value of this property must be a media type, as defined by RFC 2046 [RFC2046]. This property defines the media type of instances which this schema defines. [reference](https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.8.4) **Kind**: global function | Param | Type | | --- | --- | | mediaType | [string](#string) | ================================================ FILE: eslint.config.js ================================================ 'use strict' module.exports = require('neostandard')({ ignores: require('neostandard').resolveIgnoresFromGitignore(), ts: true }) ================================================ FILE: package.json ================================================ { "name": "fluent-json-schema", "version": "6.0.0", "description": "JSON Schema fluent API", "main": "src/FluentJSONSchema.js", "type": "commonjs", "types": "types/FluentJSONSchema.d.ts", "keywords": [ "JSON", "schema", "jsonschema", "json schema", "validation", "json schema builder", "json schema validation" ], "license": "MIT", "author": "Lorenzo Sicilia ", "contributors": [ "Matteo Collina " ], "homepage": "https://github.com/fastify/fluent-json-schema#readme", "funding": [ { "type": "github", "url": "https://github.com/sponsors/fastify" }, { "type": "opencollective", "url": "https://opencollective.com/fastify" } ], "bugs": { "url": "https://github.com/fastify/fluent-json-schema/issues" }, "repository": { "type": "git", "url": "git+https://github.com/fastify/fluent-json-schema.git" }, "scripts": { "lint": "eslint", "lint:fix": "eslint --fix", "test": "npm run test:unit && npm run test:typescript", "test:unit": "c8 --100 node --test", "test:typescript": "tsd", "doc": "jsdoc2md ./src/*.js > docs/API.md" }, "devDependencies": { "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "c8": "^11.0.0", "eslint": "^9.17.0", "jsdoc-to-markdown": "^9.0.0", "lodash.merge": "^4.6.2", "neostandard": "^0.13.0", "tsd": "^0.33.0" }, "dependencies": { "@fastify/deepmerge": "^3.0.0" } } ================================================ FILE: src/ArraySchema.js ================================================ 'use strict' const { BaseSchema } = require('./BaseSchema') const { setAttribute, isFluentSchema, FluentSchemaError } = require('./utils') const initialState = { // $schema: 'http://json-schema.org/draft-07/schema#', type: 'array', definitions: [], properties: [], required: [] } /** * Represents a ArraySchema. * @param {Object} [options] - Options * @param {StringSchema} [options.schema] - Default schema * @param {boolean} [options.generateIds = false] - generate the id automatically e.g. #properties.foo * @returns {ArraySchema} */ // https://medium.com/javascript-scene/javascript-factory-functions-with-es6-4d224591a8b1 // Factory Functions for Mixin Composition withBaseSchema const ArraySchema = ({ schema = initialState, ...options } = {}) => { options = { generateIds: false, factory: ArraySchema, ...options } return { ...BaseSchema({ ...options, schema }), /** * This keyword determines how child instances validate for arrays, and does not directly validate the immediate instance itself. * If "items" is a schema, validation succeeds if all elements in the array successfully validate against that schema. * If "items" is an array of schemas, validation succeeds if each element of the instance validates against the schema at the same position, if any. * Omitting this keyword has the same behavior as an empty schema. * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.4.1|reference} * @param {FluentSchema|FluentSchema[]} items * @returns {FluentSchema} */ items: items => { if ( !isFluentSchema(items) && !( Array.isArray(items) && items.filter(v => isFluentSchema(v)).length > 0 ) ) { throw new FluentSchemaError("'items' must be a S or an array of S") } if (Array.isArray(items)) { const values = items.map(v => { const { $schema, ...rest } = v.valueOf() return rest }) return setAttribute({ schema, ...options }, ['items', values, 'array']) } const { $schema, ...rest } = items.valueOf() return setAttribute({ schema, ...options }, [ 'items', { ...rest }, 'array' ]) }, /** * This keyword determines how child instances validate for arrays, and does not directly validate the immediate instance itself. * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.4.2|reference} * @param {FluentSchema|boolean} items * @returns {FluentSchema} */ additionalItems: items => { if (typeof items !== 'boolean' && !isFluentSchema(items)) { throw new FluentSchemaError( "'additionalItems' must be a boolean or a S" ) } if (items === false) { return setAttribute({ schema, ...options }, [ 'additionalItems', false, 'array' ]) } const { $schema, ...rest } = items.valueOf() return setAttribute({ schema, ...options }, [ 'additionalItems', { ...rest }, 'array' ]) }, /** * An array instance is valid against "contains" if at least one of its elements is valid against the given schema. * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.4.6|reference} * @param {FluentSchema} value * @returns {FluentSchema} */ contains: value => { if (!isFluentSchema(value)) { throw new FluentSchemaError("'contains' must be a S") } const { $schema, definitions, properties, required, ...rest } = value.valueOf() return setAttribute({ schema, ...options }, [ 'contains', { ...rest }, 'array' ]) }, /** * If this keyword has boolean value false, the instance validates successfully. * If it has boolean value true, the instance validates successfully if all of its elements are unique. * Omitting this keyword has the same behavior as a value of false. * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.4.5|reference} * @param {boolean} boolean * @returns {FluentSchema} */ uniqueItems: boolean => { if (typeof boolean !== 'boolean') { throw new FluentSchemaError("'uniqueItems' must be a boolean") } return setAttribute({ schema, ...options }, [ 'uniqueItems', boolean, 'array' ]) }, /** * An array instance is valid against "minItems" if its size is greater than, or equal to, the value of this keyword. * Omitting this keyword has the same behavior as a value of 0. * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.4.4|reference} * @param {number} min * @returns {FluentSchema} */ minItems: min => { if (!Number.isInteger(min)) { throw new FluentSchemaError("'minItems' must be a integer") } return setAttribute({ schema, ...options }, ['minItems', min, 'array']) }, /** * An array instance is valid against "minItems" if its size is greater than, or equal to, the value of this keyword. * Omitting this keyword has the same behavior as a value of 0. * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.4.3|reference} * @param {number} max * @returns {FluentSchema} */ maxItems: max => { if (!Number.isInteger(max)) { throw new FluentSchemaError("'maxItems' must be a integer") } return setAttribute({ schema, ...options }, ['maxItems', max, 'array']) } } } module.exports = { ArraySchema, default: ArraySchema } ================================================ FILE: src/ArraySchema.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const { ArraySchema } = require('./ArraySchema') const S = require('./FluentJSONSchema') describe('ArraySchema', () => { it('defined', () => { assert.notStrictEqual(ArraySchema, undefined) }) it('Expose symbol', () => { assert.notStrictEqual( ArraySchema()[Symbol.for('fluent-schema-object')], undefined ) }) describe('constructor', () => { it('without params', () => { assert.deepStrictEqual(ArraySchema().valueOf(), { type: 'array' }) }) it('from S', () => { assert.deepStrictEqual(S.array().valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', type: 'array' }) }) }) describe('keywords:', () => { describe('items', () => { it('valid object', () => { assert.deepStrictEqual(ArraySchema().items(S.number()).valueOf(), { type: 'array', items: { type: 'number' } }) }) it('valid array', () => { assert.deepStrictEqual( ArraySchema().items([S.number(), S.string()]).valueOf(), { type: 'array', items: [{ type: 'number' }, { type: 'string' }] } ) }) it('invalid', () => { assert.throws( () => ArraySchema().items(''), (err) => err instanceof S.FluentSchemaError && err.message === "'items' must be a S or an array of S" ) }) }) describe('additionalItems', () => { it('valid', () => { assert.deepStrictEqual( ArraySchema() .items([S.number(), S.string()]) .additionalItems(S.string()) .valueOf(), { type: 'array', items: [{ type: 'number' }, { type: 'string' }], additionalItems: { type: 'string' } } ) }) it('false', () => { assert.deepStrictEqual( ArraySchema() .items([S.number(), S.string()]) .additionalItems(false) .valueOf(), { type: 'array', items: [{ type: 'number' }, { type: 'string' }], additionalItems: false } ) }) it('invalid', () => { assert.throws( () => ArraySchema().additionalItems(''), (err) => err instanceof S.FluentSchemaError && err.message === "'additionalItems' must be a boolean or a S" ) }) }) describe('contains', () => { it('valid', () => { assert.deepStrictEqual(ArraySchema().contains(S.string()).valueOf(), { type: 'array', contains: { type: 'string' } }) }) it('invalid', () => { assert.throws( () => ArraySchema().contains('').valueOf(), (err) => err instanceof S.FluentSchemaError && err.message === "'contains' must be a S" ) }) }) describe('uniqueItems', () => { it('valid', () => { assert.deepStrictEqual(ArraySchema().uniqueItems(true).valueOf(), { type: 'array', uniqueItems: true }) }) it('invalid', () => { assert.throws( () => ArraySchema().uniqueItems('invalid').valueOf(), (err) => err instanceof S.FluentSchemaError && err.message === "'uniqueItems' must be a boolean" ) }) }) describe('minItems', () => { it('valid', () => { assert.deepStrictEqual(ArraySchema().minItems(3).valueOf(), { type: 'array', minItems: 3 }) }) it('invalid', () => { assert.throws( () => ArraySchema().minItems('3').valueOf(), (err) => err instanceof S.FluentSchemaError && err.message === "'minItems' must be a integer" ) }) }) describe('maxItems', () => { it('valid', () => { assert.deepStrictEqual(ArraySchema().maxItems(5).valueOf(), { type: 'array', maxItems: 5 }) }) it('invalid', () => { assert.throws( () => ArraySchema().maxItems('5').valueOf(), (err) => err instanceof S.FluentSchemaError && err.message === "'maxItems' must be a integer" ) }) }) describe('raw', () => { it('allows to add a custom attribute', () => { const schema = ArraySchema().raw({ customKeyword: true }).valueOf() assert.deepStrictEqual(schema, { type: 'array', customKeyword: true }) }) }) describe('default array in an object', () => { it('valid', () => { const value = [] assert.deepStrictEqual( S.object().prop('p1', ArraySchema().default(value)).valueOf() .properties.p1.default, value ) }) }) }) }) ================================================ FILE: src/BaseSchema.js ================================================ 'use strict' const { flat, omit, isFluentSchema, last, isBoolean, isUniq, patchIdsWithParentId, REQUIRED, setAttribute, setRaw, setComposeType, FluentSchemaError, FLUENT_SCHEMA } = require('./utils') const initialState = { properties: [], required: [] } /** * Represents a BaseSchema. * @param {Object} [options] - Options * @param {BaseSchema} [options.schema] - Default schema * @param {boolean} [options.generateIds = false] - generate the id automatically e.g. #properties.foo * @returns {BaseSchema} */ const BaseSchema = ( { schema = initialState, ...options } = { generateIds: false, factory: BaseSchema } ) => ({ [FLUENT_SCHEMA]: true, isFluentSchema: true, isFluentJSONSchema: true, /** * It defines a URI for the schema, and the base URI that other URI references within the schema are resolved against. * * {@link https://tools.ietf.org/html/draft-handrews-json-schema-01#section-8.2|reference} * @param {string} id - an #id * @returns {BaseSchema} */ id: id => { if (!id) { throw new FluentSchemaError( 'id should not be an empty fragment <#> or an empty string <> (e.g. #myId)' ) } return setAttribute({ schema, ...options }, ['$id', id, 'any']) }, /** * It can be used to decorate a user interface with information about the data produced by this user interface. A title will preferably be short. * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.10.1|reference} * @param {string} title * @returns {BaseSchema} */ title: title => { return setAttribute({ schema, ...options }, ['title', title, 'any']) }, /** * It can be used to decorate a user interface with information about the data * produced by this user interface. A description provides explanation about * the purpose of the instance described by the schema. * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.10.1|reference} * @param {string} description * @returns {BaseSchema} */ description: description => { return setAttribute({ schema, ...options }, [ 'description', description, 'any' ]) }, /** * The value of this keyword MUST be an array. * There are no restrictions placed on the values within the array. * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.10.4|reference} * @param {string} examples * @returns {BaseSchema} */ examples: examples => { if (!Array.isArray(examples)) { throw new FluentSchemaError( "'examples' must be an array e.g. ['1', 'one', 'foo']" ) } return setAttribute({ schema, ...options }, ['examples', examples, 'any']) }, /** * The value must be a valid id e.g. #properties/foo * * @param {string} ref * @returns {BaseSchema} */ ref: ref => { return setAttribute({ schema, ...options }, ['$ref', ref, 'any']) }, /** * The value of this keyword MUST be an array. This array SHOULD have at least one element. Elements in the array SHOULD be unique. * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.1.2|reference} * @param {array} values * @returns {BaseSchema} */ enum: values => { if (!Array.isArray(values)) { throw new FluentSchemaError( "'enums' must be an array with at least an element e.g. ['1', 'one', 'foo']" ) } return setAttribute({ schema, ...options }, ['enum', values, 'any']) }, /** * The value of this keyword MAY be of any type, including null. * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.1.3|reference} * @param value * @returns {BaseSchema} */ const: value => { return setAttribute({ schema, ...options }, ['const', value, 'any']) }, /** * There are no restrictions placed on the value of this keyword. * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.10.2|reference} * @param defaults * @returns {BaseSchema} */ default: defaults => { return setAttribute({ schema, ...options }, ['default', defaults, 'any']) }, /** * The value of readOnly can be left empty to indicate the property is readOnly. * It takes an optional boolean which can be used to explicitly set readOnly true/false. * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.10.3|reference} * @param {boolean|undefined} isReadOnly * @returns {BaseSchema} */ readOnly: isReadOnly => { const value = isReadOnly !== undefined ? isReadOnly : true return setAttribute({ schema, ...options }, ['readOnly', value, 'boolean']) }, /** * The value of writeOnly can be left empty to indicate the property is writeOnly. * It takes an optional boolean which can be used to explicitly set writeOnly true/false. * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.10.3|reference} * @param {boolean|undefined} isWriteOnly * @returns {BaseSchema} */ writeOnly: isWriteOnly => { const value = isWriteOnly !== undefined ? isWriteOnly : true return setAttribute({ schema, ...options }, ['writeOnly', value, 'boolean']) }, /** * The value of deprecated can be left empty to indicate the property is deprecated. * It takes an optional boolean which can be used to explicitly set deprecated true/false. * * {@link https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.9.3|reference} * @param {Boolean} isDeprecated * @returns {BaseSchema} */ deprecated: (isDeprecated) => { if (isDeprecated && !isBoolean(isDeprecated)) throw new FluentSchemaError("'deprecated' must be a boolean value") const value = isDeprecated !== undefined ? isDeprecated : true return setAttribute({ schema, ...options }, ['deprecated', value, 'boolean']) }, /** * Required has to be chained to a property: * Examples: * - S.prop('prop').required() * - S.prop('prop', S.number()).required() * - S.required(['foo', 'bar']) * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.5.3|reference} * @returns {FluentSchema} */ required: props => { const currentProp = last(schema.properties) const required = Array.isArray(props) ? [...schema.required, ...props] : currentProp ? [...schema.required, currentProp.name] : [REQUIRED] if (!isUniq(required)) { throw new FluentSchemaError("'required' has repeated keys, check your calls to .required()") } return options.factory({ schema: { ...schema, required }, ...options }) }, /** * This keyword's value MUST be a valid JSON Schema. * An instance is valid against this keyword if it fails to validate successfully against the schema defined by this keyword. * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.7.4|reference} * @param {FluentSchema} not * @returns {BaseSchema} */ not: not => { if (!isFluentSchema(not)) { throw new FluentSchemaError("'not' must be a BaseSchema") } const notSchema = omit(not.valueOf(), ['$schema', 'definitions']) return BaseSchema({ schema: { ...schema, not: patchIdsWithParentId({ schema: notSchema, ...options, parentId: '#not' }) }, ...options }) }, // return setAttribute({ schema, ...options }, ['defaults', defaults, 'any']) /** * It MUST be a non-empty array. Each item of the array MUST be a valid JSON Schema. * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.7.2|reference} * @param {array} schemas * @returns {BaseSchema} */ anyOf: schemas => setComposeType({ prop: 'anyOf', schemas, schema, options }), /** * It MUST be a non-empty array. Each item of the array MUST be a valid JSON Schema. * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.7.1|reference} * @param {array} schemas * @returns {BaseSchema} */ allOf: schemas => setComposeType({ prop: 'allOf', schemas, schema, options }), /** * It MUST be a non-empty array. Each item of the array MUST be a valid JSON Schema. * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.7.3|reference} * @param {array} schemas * @returns {BaseSchema} */ oneOf: schemas => setComposeType({ prop: 'oneOf', schemas, schema, options }), /** * @private set a property to a type. Use string number etc. * @returns {BaseSchema} */ as: type => setAttribute({ schema, ...options }, ['type', type]), /** * This validation outcome of this keyword's subschema has no direct effect on the overall validation result. * Rather, it controls which of the "then" or "else" keywords are evaluated. * When "if" is present, and the instance successfully validates against its subschema, then * validation succeeds against this keyword if the instance also successfully validates against this keyword's subschema. * * @param {BaseSchema} ifClause * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.6.1|reference} * @param {BaseSchema} thenClause * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.6.2|reference} * @returns {BaseSchema} */ ifThen: (ifClause, thenClause) => { if (!isFluentSchema(ifClause)) { throw new FluentSchemaError("'ifClause' must be a BaseSchema") } if (!isFluentSchema(thenClause)) { throw new FluentSchemaError("'thenClause' must be a BaseSchema") } const ifClauseSchema = omit(ifClause.valueOf(), [ '$schema', 'definitions', 'type' ]) const thenClauseSchema = omit(thenClause.valueOf(), [ '$schema', 'definitions', 'type' ]) return options.factory({ schema: { ...schema, if: patchIdsWithParentId({ schema: ifClauseSchema, ...options, parentId: '#if' }), then: patchIdsWithParentId({ schema: thenClauseSchema, ...options, parentId: '#then' }) }, ...options }) }, /** * When "if" is present, and the instance fails to validate against its subschema, * then validation succeeds against this keyword if the instance successfully validates against this keyword's subschema. * * @param {BaseSchema} ifClause * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.6.1|reference} * @param {BaseSchema} thenClause * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.6.2|reference} * @param {BaseSchema} elseClause * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.6.3|reference} * @returns {BaseSchema} */ ifThenElse: (ifClause, thenClause, elseClause) => { if (!isFluentSchema(ifClause)) { throw new FluentSchemaError("'ifClause' must be a BaseSchema") } if (!isFluentSchema(thenClause)) { throw new FluentSchemaError("'thenClause' must be a BaseSchema") } if (!isFluentSchema(elseClause)) { throw new FluentSchemaError( "'elseClause' must be a BaseSchema or a false boolean value" ) } const ifClauseSchema = omit(ifClause.valueOf(), [ '$schema', 'definitions', 'type' ]) const thenClauseSchema = omit(thenClause.valueOf(), [ '$schema', 'definitions', 'type' ]) const elseClauseSchema = omit(elseClause.valueOf(), [ '$schema', 'definitions', 'type' ]) return options.factory({ schema: { ...schema, if: patchIdsWithParentId({ schema: ifClauseSchema, ...options, parentId: '#if' }), then: patchIdsWithParentId({ schema: thenClauseSchema, ...options, parentId: '#then' }), else: patchIdsWithParentId({ schema: elseClauseSchema, ...options, parentId: '#else' }) }, ...options }) }, /** * Because the differences between JSON Schemas and Open API (Swagger) * it can be handy to arbitrary modify the schema injecting a fragment * * * Examples: * - S.number().raw({ nullable:true }) * - S.string().format('date').raw({ formatMaximum: '2020-01-01' }) * * @param {string} fragment an arbitrary JSON Schema to inject * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.3.3|reference} * @returns {BaseSchema} */ raw: fragment => { return setRaw({ schema, ...options }, fragment) }, /** * @private It returns the internal schema data structure * @returns {object} */ // TODO LS if we implement S.raw() we can drop this hack because from a JSON we can rebuild a fluent-json-schema _getState: () => { return schema }, /** * It returns all the schema values * * @param {Object} [options] - Options * @param {boolean} [options.isRoot = true] - Is a root level schema * @returns {object} */ valueOf: ({ isRoot } = { isRoot: true }) => { const { properties, definitions, required, $schema, ...rest } = schema if (isRoot && required && !required.every((v) => typeof v === 'string')) { throw new FluentSchemaError("'required' has called on root-level schema, check your calls to .required()") } return Object.assign( $schema ? { $schema } : {}, Object.keys(definitions || []).length > 0 ? { definitions: flat(definitions) } : undefined, { ...omit(rest, ['if', 'then', 'else']) }, Object.keys(properties || []).length > 0 ? { properties: flat(properties) } : undefined, required && required.length > 0 ? { required } : undefined, schema.if ? { if: schema.if } : undefined, schema.then ? { then: schema.then } : undefined, schema.else ? { else: schema.else } : undefined ) } }) module.exports = { BaseSchema, default: BaseSchema } ================================================ FILE: src/BaseSchema.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const { BaseSchema } = require('./BaseSchema') const S = require('./FluentJSONSchema') describe('BaseSchema', () => { it('defined', () => { assert.notStrictEqual(BaseSchema, undefined) }) it('Expose symbol', () => { assert.notStrictEqual( BaseSchema()[Symbol.for('fluent-schema-object')], undefined ) }) it('Expose legacy plain boolean', () => { assert.notStrictEqual(BaseSchema().isFluentSchema, undefined) }) it('Expose plain boolean', () => { assert.notStrictEqual(BaseSchema().isFluentJSONSchema, undefined) }) describe('factory', () => { it('without params', () => { assert.deepStrictEqual(BaseSchema().valueOf(), {}) }) describe('factory', () => { it('default', () => { const title = 'title' assert.deepStrictEqual(BaseSchema().title(title).valueOf(), { title }) }) it('override', () => { const title = 'title' assert.deepStrictEqual( BaseSchema({ factory: BaseSchema }).title(title).valueOf(), { title } ) }) }) }) describe('keywords (any):', () => { describe('id', () => { const value = 'customId' it('to root', () => { assert.strictEqual(BaseSchema().id(value).valueOf().$id, value) }) it('nested', () => { assert.strictEqual( S.object().prop('foo', BaseSchema().id(value).required()).valueOf() .properties.foo.$id, value ) }) it('invalid', () => { assert.throws( () => BaseSchema().id(''), (err) => err instanceof S.FluentSchemaError && err.message === 'id should not be an empty fragment <#> or an empty string <> (e.g. #myId)' ) }) }) describe('title', () => { const value = 'title' it('adds to root', () => { assert.strictEqual(BaseSchema().title(value).valueOf().title, value) }) }) describe('description', () => { it('add to root', () => { const value = 'description' assert.strictEqual( BaseSchema().description(value).valueOf().description, value ) }) }) describe('examples', () => { it('adds to root', () => { const value = ['example'] assert.deepStrictEqual( BaseSchema().examples(value).valueOf().examples, value ) }) it('invalid', () => { const value = 'examples' assert.throws( () => BaseSchema().examples(value).valueOf().examples, (err) => err instanceof S.FluentSchemaError && err.message === "'examples' must be an array e.g. ['1', 'one', 'foo']" ) }) }) describe('required', () => { it('in line valid', () => { const prop = 'foo' assert.deepStrictEqual( S.object().prop(prop).required().valueOf().required, [prop] ) }) it('nested valid', () => { const prop = 'foo' assert.deepStrictEqual( S.object().prop(prop, S.string().required().minLength(3)).valueOf() .required, [prop] ) }) describe('unique keys on required', () => { it('repeated calls to required()', () => { assert.throws( () => S.object().prop('A', S.string()).required().required(), (err) => err instanceof S.FluentSchemaError && err.message === "'required' has repeated keys, check your calls to .required()" ) }) it('repeated props on appendRequired()', () => { assert.throws( () => S.object() .prop('A', S.string().required()) .prop('A', S.string().required()), (err) => err instanceof S.FluentSchemaError && err.message === "'required' has repeated keys, check your calls to .required()" ) }) }) it('root-level required', () => { assert.throws( () => S.object().required().valueOf(), (err) => err instanceof S.FluentSchemaError && err.message === "'required' has called on root-level schema, check your calls to .required()" ) }) describe('array', () => { it('simple', () => { const required = ['foo', 'bar'] assert.deepStrictEqual(S.required(required).valueOf(), { required }) }) it('nested', () => { assert.deepStrictEqual( S.object() .prop('foo', S.string()) .prop('bar', S.string().required()) .required(['foo']) .valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', properties: { bar: { type: 'string' }, foo: { type: 'string' } }, required: ['bar', 'foo'], type: 'object' } ) }) }) }) describe('deprecated', () => { it('valid', () => { assert.strictEqual( BaseSchema().deprecated(true).valueOf().deprecated, true ) }) it('invalid', () => { assert.throws( () => BaseSchema().deprecated('somethingNotBoolean').valueOf().deprecated, (err) => err instanceof S.FluentSchemaError && err.message === "'deprecated' must be a boolean value" ) }) it('valid with no value', () => { assert.strictEqual(BaseSchema().deprecated().valueOf().deprecated, true) }) it('can be set to false', () => { assert.strictEqual( BaseSchema().deprecated(false).valueOf().deprecated, false ) }) it('property', () => { assert.deepStrictEqual( S.object() .prop('foo', S.string()) .prop('bar', S.string().deprecated()) .valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', properties: { bar: { type: 'string', deprecated: true }, foo: { type: 'string' } }, type: 'object' } ) }) it('object', () => { assert.deepStrictEqual( S.object() .prop('foo', S.string()) .prop( 'bar', S.object() .deprecated() .prop('raz', S.string()) .prop('iah', S.number()) ) .valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', properties: { foo: { type: 'string' }, bar: { type: 'object', deprecated: true, properties: { raz: { $id: undefined, type: 'string' }, iah: { $id: undefined, type: 'number' } } } }, type: 'object' } ) }) it('object property', () => { assert.deepStrictEqual( S.object() .prop('foo', S.string()) .prop( 'bar', S.object() .prop('raz', S.string().deprecated()) .prop('iah', S.number()) ) .valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', properties: { foo: { type: 'string' }, bar: { type: 'object', properties: { raz: { $id: undefined, type: 'string', deprecated: true }, iah: { $id: undefined, type: 'number' } } } }, type: 'object' } ) }) it('array', () => { assert.deepStrictEqual( S.object() .prop('foo', S.string()) .prop('bar', S.array().deprecated().items(S.number())) .valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', type: 'object', properties: { foo: { type: 'string' }, bar: { type: 'array', deprecated: true, items: { type: 'number' } } } } ) }) it('array item', () => { assert.deepStrictEqual( S.object() .prop('foo', S.string()) .prop( 'bar', S.array().items([ S.object().prop('zoo', S.string()).prop('biz', S.string()), S.object() .deprecated() .prop('zal', S.string()) .prop('boz', S.string()) ]) ) .valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', type: 'object', properties: { foo: { type: 'string' }, bar: { type: 'array', items: [ { type: 'object', properties: { zoo: { type: 'string' }, biz: { type: 'string' } } }, { type: 'object', deprecated: true, properties: { zal: { type: 'string' }, boz: { type: 'string' } } } ] } } } ) }) }) describe('enum', () => { it('valid', () => { const value = ['VALUE'] assert.deepStrictEqual(BaseSchema().enum(value).valueOf().enum, value) }) it('invalid', () => { const value = 'VALUE' assert.throws( () => BaseSchema().enum(value).valueOf().examples, (err) => err instanceof S.FluentSchemaError && err.message === "'enums' must be an array with at least an element e.g. ['1', 'one', 'foo']" ) }) }) describe('const', () => { it('valid', () => { const value = 'VALUE' assert.strictEqual(BaseSchema().const(value).valueOf().const, value) }) }) describe('default', () => { it('valid', () => { const value = 'VALUE' assert.strictEqual(BaseSchema().default(value).valueOf().default, value) }) }) describe('readOnly', () => { it('valid', () => { assert.strictEqual(BaseSchema().readOnly(true).valueOf().readOnly, true) }) it('valid with no value', () => { assert.strictEqual(BaseSchema().readOnly().valueOf().readOnly, true) }) it('can be set to false', () => { assert.strictEqual( BaseSchema().readOnly(false).valueOf().readOnly, false ) }) }) describe('writeOnly', () => { it('valid', () => { assert.strictEqual( BaseSchema().writeOnly(true).valueOf().writeOnly, true ) }) it('valid with no value', () => { assert.strictEqual(BaseSchema().writeOnly().valueOf().writeOnly, true) }) it('can be set to false', () => { assert.strictEqual( BaseSchema().writeOnly(false).valueOf().writeOnly, false ) }) }) describe('ref', () => { it('base', () => { const ref = 'myRef' assert.deepStrictEqual(BaseSchema().ref(ref).valueOf(), { $ref: ref }) }) it('S', () => { const ref = 'myRef' assert.deepStrictEqual(S.ref(ref).valueOf(), { $ref: ref }) }) }) }) describe('combining keywords:', () => { describe('allOf', () => { it('base', () => { assert.deepStrictEqual( BaseSchema() .allOf([BaseSchema().id('foo')]) .valueOf(), { allOf: [{ $id: 'foo' }] } ) }) it('S', () => { assert.deepStrictEqual(S.allOf([S.id('foo')]).valueOf(), { allOf: [{ $id: 'foo' }] }) }) describe('invalid', () => { it('not an array', () => { assert.throws( () => BaseSchema().allOf('test'), (err) => err instanceof S.FluentSchemaError && err.message === "'allOf' must be a an array of FluentSchema rather than a 'string'" ) }) it('not an array of FluentSchema', () => { assert.throws( () => BaseSchema().allOf(['test']), (err) => err instanceof S.FluentSchemaError && err.message === "'allOf' must be a an array of FluentSchema rather than a 'object'" ) }) }) }) describe('anyOf', () => { it('valid', () => { assert.deepStrictEqual( BaseSchema() .anyOf([BaseSchema().id('foo')]) .valueOf(), { anyOf: [{ $id: 'foo' }] } ) }) it('S nested', () => { assert.deepStrictEqual( S.object() .prop('prop', S.anyOf([S.string(), S.null()])) .valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', properties: { prop: { anyOf: [{ type: 'string' }, { type: 'null' }] } }, type: 'object' } ) }) it('S nested required', () => { assert.deepStrictEqual( S.object().prop('prop', S.anyOf([]).required()).valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', properties: { prop: {} }, required: ['prop'], type: 'object' } ) }) describe('invalid', () => { it('not an array', () => { assert.throws( () => BaseSchema().anyOf('test'), (err) => err instanceof S.FluentSchemaError && err.message === "'anyOf' must be a an array of FluentSchema rather than a 'string'" ) }) it('not an array of FluentSchema', () => { assert.throws( () => BaseSchema().anyOf(['test']), (err) => err instanceof S.FluentSchemaError && err.message === "'anyOf' must be a an array of FluentSchema rather than a 'object'" ) }) }) }) describe('oneOf', () => { it('valid', () => { assert.deepStrictEqual( BaseSchema() .oneOf([BaseSchema().id('foo')]) .valueOf(), { oneOf: [{ $id: 'foo' }] } ) }) describe('invalid', () => { it('not an array', () => { assert.throws( () => BaseSchema().oneOf('test'), (err) => err instanceof S.FluentSchemaError && err.message === "'oneOf' must be a an array of FluentSchema rather than a 'string'" ) }) it('not an array of FluentSchema', () => { assert.throws( () => BaseSchema().oneOf(['test']), (err) => err instanceof S.FluentSchemaError && err.message === "'oneOf' must be a an array of FluentSchema rather than a 'object'" ) }) }) }) describe('not', () => { describe('valid', () => { it('simple', () => { assert.deepStrictEqual( BaseSchema().not(S.string().maxLength(10)).valueOf(), { not: { type: 'string', maxLength: 10 } } ) }) it('complex', () => { assert.deepStrictEqual( BaseSchema() .not(BaseSchema().anyOf([BaseSchema().id('foo')])) .valueOf(), { not: { anyOf: [{ $id: 'foo' }] } } ) }) // .prop('notTypeKey', S.not(S.string().maxLength(10))) => notTypeKey: { not: { type: 'string', "maxLength": 10 } } }) it('invalid', () => { assert.throws( () => BaseSchema().not(undefined), (err) => err instanceof S.FluentSchemaError && err.message === "'not' must be a BaseSchema" ) }) }) }) describe('ifThen', () => { describe('valid', () => { it('returns a schema', () => { const id = 'http://foo.com/user' const schema = BaseSchema() .id(id) .title('A User') .ifThen(BaseSchema().id(id), BaseSchema().description('A User desc')) .valueOf() assert.deepStrictEqual(schema, { $id: 'http://foo.com/user', title: 'A User', if: { $id: 'http://foo.com/user' }, then: { description: 'A User desc' } }) }) it('appends a prop after the clause', () => { const id = 'http://foo.com/user' const schema = S.object() .id(id) .title('A User') .prop('bar') .ifThen( S.object().prop('foo', S.null()), S.object().prop('bar', S.string().required()) ) .prop('foo') .valueOf() assert.deepStrictEqual(schema, { $schema: 'http://json-schema.org/draft-07/schema#', type: 'object', $id: 'http://foo.com/user', title: 'A User', properties: { bar: {}, foo: {} }, if: { properties: { foo: { $id: undefined, type: 'null' } } }, then: { properties: { bar: { $id: undefined, type: 'string' } }, required: ['bar'] } }) }) }) describe('invalid', () => { it('ifClause', () => { assert.throws( () => BaseSchema().ifThen( undefined, BaseSchema().description('A User desc') ), (err) => err instanceof S.FluentSchemaError && err.message === "'ifClause' must be a BaseSchema" ) }) it('thenClause', () => { assert.throws( () => BaseSchema().ifThen(BaseSchema().id('id'), undefined), (err) => err instanceof S.FluentSchemaError && err.message === "'thenClause' must be a BaseSchema" ) }) }) }) describe('ifThenElse', () => { describe('valid', () => { it('returns a schema', () => { const id = 'http://foo.com/user' const schema = BaseSchema() .id(id) .title('A User') .ifThenElse( BaseSchema().id(id), BaseSchema().description('then'), BaseSchema().description('else') ) .valueOf() assert.deepStrictEqual(schema, { $id: 'http://foo.com/user', title: 'A User', if: { $id: 'http://foo.com/user' }, then: { description: 'then' }, else: { description: 'else' } }) }) it('appends a prop after the clause', () => { const id = 'http://foo.com/user' const schema = S.object() .id(id) .title('A User') .prop('bar') .ifThenElse( S.object().prop('foo', S.null()), S.object().prop('bar', S.string().required()), S.object().prop('bar', S.string()) ) .prop('foo') .valueOf() assert.deepStrictEqual(schema, { $schema: 'http://json-schema.org/draft-07/schema#', type: 'object', $id: 'http://foo.com/user', title: 'A User', properties: { bar: {}, foo: {} }, if: { properties: { foo: { $id: undefined, type: 'null' } } }, then: { properties: { bar: { $id: undefined, type: 'string' } }, required: ['bar'] }, else: { properties: { bar: { $id: undefined, type: 'string' } } } }) }) describe('invalid', () => { it('ifClause', () => { assert.throws( () => BaseSchema().ifThenElse( undefined, BaseSchema().description('then'), BaseSchema().description('else') ), (err) => err instanceof S.FluentSchemaError && err.message === "'ifClause' must be a BaseSchema" ) }) it('thenClause', () => { assert.throws( () => BaseSchema().ifThenElse( BaseSchema().id('id'), undefined, BaseSchema().description('else') ), (err) => err instanceof S.FluentSchemaError && err.message === "'thenClause' must be a BaseSchema" ) }) it('elseClause', () => { assert.throws( () => BaseSchema().ifThenElse( BaseSchema().id('id'), BaseSchema().description('then'), undefined ), (err) => err instanceof S.FluentSchemaError && err.message === "'elseClause' must be a BaseSchema or a false boolean value" ) }) }) }) }) describe('raw', () => { it('allows to add a custom attribute', () => { const schema = BaseSchema() .title('foo') .raw({ customKeyword: true }) .valueOf() assert.deepStrictEqual(schema, { title: 'foo', customKeyword: true }) }) }) }) ================================================ FILE: src/BooleanSchema.js ================================================ 'use strict' const { BaseSchema } = require('./BaseSchema') const initialState = { type: 'boolean' } /** * Represents a BooleanSchema. * @param {Object} [options] - Options * @param {StringSchema} [options.schema] - Default schema * @param {boolean} [options.generateIds = false] - generate the id automatically e.g. #properties.foo * @returns {StringSchema} */ const BooleanSchema = ({ schema = initialState, ...options } = {}) => { options = { generateIds: false, factory: BaseSchema, ...options } return { ...BaseSchema({ ...options, schema }) } } module.exports = { BooleanSchema, default: BooleanSchema } ================================================ FILE: src/BooleanSchema.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const { BooleanSchema } = require('./BooleanSchema') const S = require('./FluentJSONSchema') describe('BooleanSchema', () => { it('defined', () => { assert.notStrictEqual(BooleanSchema, undefined) }) it('Expose symbol', () => { assert.notStrictEqual( BooleanSchema()[Symbol.for('fluent-schema-object')], undefined ) }) describe('constructor', () => { it('without params', () => { assert.deepStrictEqual(BooleanSchema().valueOf(), { type: 'boolean' }) }) it('from S', () => { assert.deepStrictEqual(S.boolean().valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', type: 'boolean' }) }) }) it('sets a null type to the prop', () => { assert.strictEqual( S.object().prop('prop', S.boolean()).valueOf().properties.prop.type, 'boolean' ) }) describe('raw', () => { it('allows to add a custom attribute', () => { const schema = BooleanSchema().raw({ customKeyword: true }).valueOf() assert.deepStrictEqual(schema, { type: 'boolean', customKeyword: true }) }) }) }) ================================================ FILE: src/FluentJSONSchema.js ================================================ 'use strict' const { FORMATS, TYPES, FluentSchemaError } = require('./utils') const { BaseSchema } = require('./BaseSchema') const { NullSchema } = require('./NullSchema') const { BooleanSchema } = require('./BooleanSchema') const { StringSchema } = require('./StringSchema') const { NumberSchema } = require('./NumberSchema') const { IntegerSchema } = require('./IntegerSchema') const { ObjectSchema } = require('./ObjectSchema') const { ArraySchema } = require('./ArraySchema') const { MixedSchema } = require('./MixedSchema') const { RawSchema } = require('./RawSchema') const initialState = { $schema: 'http://json-schema.org/draft-07/schema#', definitions: [], properties: [], required: [] } /** * Represents a S. * @param {Object} [options] - Options * @param {S} [options.schema] - Default schema * @param {boolean} [options.generateIds = false] - generate the id automatically e.g. #properties.foo * @returns {S} */ const S = ( { schema = initialState, ...options } = { generateIds: false, factory: BaseSchema } ) => ({ ...BaseSchema({ ...options, schema }), /** * Set a property to type string * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.3|reference} * @returns {StringSchema} */ string: () => StringSchema({ ...options, schema, factory: StringSchema }).as('string'), /** * Set a property to type number * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#numeric|reference} * @returns {NumberSchema} */ number: () => NumberSchema({ ...options, schema, factory: NumberSchema }).as('number'), /** * Set a property to type integer * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#numeric|reference} * @returns {IntegerSchema} */ integer: () => IntegerSchema({ ...options, schema, factory: IntegerSchema }).as('integer'), /** * Set a property to type boolean * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.7|reference} * @returns {BooleanSchema} */ boolean: () => BooleanSchema({ ...options, schema, factory: BooleanSchema }).as('boolean'), /** * Set a property to type array * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.4|reference} * @returns {ArraySchema} */ array: () => ArraySchema({ ...options, schema, factory: ArraySchema }).as('array'), /** * Set a property to type object * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.5|reference} * @returns {ObjectSchema} */ object: baseSchema => ObjectSchema({ ...options, schema: baseSchema || schema, factory: ObjectSchema }).as('object'), /** * Set a property to type null * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#general|reference} * @returns {NullSchema} */ null: () => NullSchema({ ...options, schema, factory: NullSchema }).null(), /** * A mixed schema is the union of multiple types (e.g. ['string', 'integer'] * * @param {Array.} types * @returns {MixedSchema} */ mixed: types => { if ( !Array.isArray(types) || (Array.isArray(types) && types.filter(t => !Object.values(TYPES).includes(t)).length > 0) ) { throw new FluentSchemaError( `Invalid 'types'. It must be an array of types. Valid types are ${Object.values( TYPES ).join(' | ')}` ) } return MixedSchema({ ...options, schema: { ...schema, type: types }, factory: MixedSchema }) }, /** * Because the differences between JSON Schemas and Open API (Swagger) * it can be handy to arbitrary modify the schema injecting a fragment * * * Examples: * - S.raw({ nullable:true, format: 'date', formatMaximum: '2020-01-01' }) * - S.string().format('date').raw({ formatMaximum: '2020-01-01' }) * * @param {string} fragment an arbitrary JSON Schema to inject * @returns {BaseSchema} */ raw: fragment => { return RawSchema(fragment) } }) const fluentSchema = { ...BaseSchema(), FORMATS, TYPES, FluentSchemaError, withOptions: S, string: () => S().string(), mixed: types => S().mixed(types), object: () => S().object(), array: () => S().array(), boolean: () => S().boolean(), integer: () => S().integer(), number: () => S().number(), null: () => S().null(), raw: fragment => S().raw(fragment) } module.exports = fluentSchema module.exports.default = fluentSchema module.exports.S = fluentSchema ================================================ FILE: src/FluentSchema.integration.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const Ajv = require('ajv') const basic = require('./schemas/basic') const S = require('./FluentJSONSchema') // TODO pick some ideas from here:https://github.com/json-schema-org/JSON-Schema-Test-Suite/tree/main/tests/draft7 describe('S', () => { it('compiles', () => { const ajv = new Ajv() const schema = S.valueOf() const validate = ajv.compile(schema) const valid = validate({}) assert.ok(valid) }) describe('basic', () => { const ajv = new Ajv() const schema = S.object() .prop('username', S.string()) .prop('password', S.string()) .valueOf() const validate = ajv.compile(schema) it('valid', () => { const valid = validate({ username: 'username', password: 'password' }) assert.ok(valid) }) it('invalid', () => { const valid = validate({ username: 'username', password: 1 }) assert.deepStrictEqual(validate.errors, [ { instancePath: '/password', keyword: 'type', message: 'must be string', params: { type: 'string' }, schemaPath: '#/properties/password/type' } ]) assert.ok(!valid) }) }) describe('ifThen', () => { const ajv = new Ajv() const schema = S.object() .prop('prop', S.string().maxLength(5)) .ifThen( S.object().prop('prop', S.string().maxLength(5)), S.object().prop('extraProp', S.string()).required() ) .valueOf() const validate = ajv.compile(schema) it('valid', () => { const valid = validate({ prop: '12345', extraProp: 'foo' }) assert.ok(valid) }) it('invalid', () => { const valid = validate({ prop: '12345' }) assert.deepStrictEqual(validate.errors, [ { instancePath: '', keyword: 'required', message: "must have required property 'extraProp'", params: { missingProperty: 'extraProp' }, schemaPath: '#/then/required' } ]) assert.ok(!valid) }) }) describe('ifThenElse', () => { const ajv = new Ajv() const VALUES = ['ONE', 'TWO'] const schema = S.object() .prop('ifProp') .ifThenElse( S.object().prop('ifProp', S.string().enum([VALUES[0]])), S.object().prop('thenProp', S.string()).required(), S.object().prop('elseProp', S.string()).required() ) .valueOf() const validate = ajv.compile(schema) it('then', () => { const valid = validate({ ifProp: 'ONE', thenProp: 'foo' }) assert.ok(valid) }) it('else', () => { const valid = validate({ prop: '123456' }) assert.deepStrictEqual(validate.errors, [ { instancePath: '', keyword: 'required', message: "must have required property 'thenProp'", params: { missingProperty: 'thenProp' }, schemaPath: '#/then/required' } ]) assert.ok(!valid) }) }) describe('combine and definition', () => { const ajv = new Ajv() const schema = S.object() // FIXME LS it shouldn't be object() .definition( 'address', S.object() .id('#/definitions/address') .prop('street_address', S.string()) .required() .prop('city', S.string()) .required() .prop('state', S.string().required()) ) .allOf([ S.ref('#/definitions/address'), S.object().prop('type', S.string()).enum(['residential', 'business']) ]) .valueOf() const validate = ajv.compile(schema) it('matches', () => { assert.deepStrictEqual(schema, { $schema: 'http://json-schema.org/draft-07/schema#', type: 'object', definitions: { address: { $id: '#/definitions/address', type: 'object', properties: { street_address: { type: 'string' }, city: { type: 'string' }, state: { type: 'string' } }, required: ['street_address', 'city', 'state'] } }, allOf: [ { $ref: '#/definitions/address' }, { type: 'object', properties: { type: { type: 'string', enum: ['residential', 'business'] } } } ] }) }) it('valid', () => { const valid = validate({ street_address: 'via Paolo Rossi', city: 'Topolinia', state: 'Disney World', type: 'business' }) assert.strictEqual(validate.errors, null) assert.ok(valid) }) }) // https://github.com/fastify/fluent-json-schema/pull/40 describe('cloning objects retains boolean', () => { const ajv = new Ajv() const config = { schema: S.object().prop('foo', S.string().enum(['foo'])) } const _config = require('lodash.merge')({}, config) const schema = _config.schema.valueOf() const validate = ajv.compile(schema) it('matches', () => { assert.notStrictEqual( config.schema[Symbol.for('fluent-schema-object')], undefined ) assert.ok(_config.schema.isFluentJSONSchema) assert.strictEqual( _config.schema[Symbol.for('fluent-schema-object')], undefined ) assert.deepStrictEqual(schema, { $schema: 'http://json-schema.org/draft-07/schema#', type: 'object', properties: { foo: { type: 'string', enum: ['foo'] } } }) }) it('valid', () => { const valid = validate({ foo: 'foo' }) assert.strictEqual(validate.errors, null) assert.ok(valid) }) }) describe('compose keywords', () => { const ajv = new Ajv() const schema = S.object() .prop('foo', S.anyOf([S.string()])) .prop('bar', S.not(S.anyOf([S.integer()]))) .prop('prop', S.allOf([S.string(), S.boolean()])) .prop('anotherProp', S.oneOf([S.string(), S.boolean()])) .required() .valueOf() const validate = ajv.compile(schema) it('valid', () => { const valid = validate({ foo: 'foo', anotherProp: true }) assert.ok(valid) }) it('invalid', () => { const valid = validate({ foo: 'foo', bar: 1 }) assert.ok(!valid) }) }) describe('compose ifThen', () => { const ajv = new Ajv() const schema = S.object() .prop('foo', S.string().default(false).required()) .prop('bar', S.string().default(false).required()) .prop('thenFooA', S.string()) .prop('thenFooB', S.string()) .allOf([ S.ifThen( S.object().prop('foo', S.string()).enum(['foo']), S.required(['thenFooA', 'thenFooB']) ), S.ifThen( S.object().prop('bar', S.string()).enum(['BAR']), S.required(['thenBarA', 'thenBarB']) ) ]) .valueOf() const validate = ajv.compile(schema) it('matches', () => { assert.deepStrictEqual(schema, { $schema: 'http://json-schema.org/draft-07/schema#', allOf: [ { if: { properties: { foo: { $id: undefined, enum: ['foo'], type: 'string' } } }, then: { required: ['thenFooA', 'thenFooB'] } }, { if: { properties: { bar: { $id: undefined, enum: ['BAR'], type: 'string' } } }, then: { required: ['thenBarA', 'thenBarB'] } } ], properties: { bar: { default: false, type: 'string' }, foo: { default: false, type: 'string' }, thenFooA: { type: 'string' }, thenFooB: { type: 'string' } }, required: ['foo', 'bar'], type: 'object' }) }) it('valid', () => { const valid = validate({ foo: 'foo', thenFooA: 'thenFooA', thenFooB: 'thenFooB', bar: 'BAR', thenBarA: 'thenBarA', thenBarB: 'thenBarB' }) assert.strictEqual(validate.errors, null) assert.ok(valid) }) }) describe('complex', () => { const ajv = new Ajv() const schema = S.object() .id('http://foo.com/user') .title('A User') .description('A User desc') .definition( 'address', S.object() .id('#address') .prop('country', S.string()) .prop('city', S.string()) .prop('zipcode', S.string()) ) .prop('username', S.string()) .required() .prop('password', S.string().required()) .prop('address', S.object().ref('#address')) .required() .prop( 'role', S.object() .id('http://foo.com/role') .required() .prop('name', S.string()) .prop('permissions') ) .prop('age', S.number()) .valueOf() const validate = ajv.compile(schema) it('valid', () => { const valid = validate({ username: 'aboutlo', password: 'pwsd', address: { country: 'Italy', city: 'Milan', zipcode: '20100' }, role: { name: 'admin', permissions: 'read:write' }, age: 30 }) assert.ok(valid) }) describe('invalid', () => { const model = { username: 'aboutlo', password: 'pswd', address: { country: 'Italy', city: 'Milan', zipcode: '20100' }, role: { name: 'admin', permissions: 'read:write' }, age: 30 } it('password', () => { const { password, ...data } = model const valid = validate(data) assert.deepStrictEqual(validate.errors, [ { instancePath: '', keyword: 'required', message: "must have required property 'password'", params: { missingProperty: 'password' }, schemaPath: '#/required' } ]) assert.ok(!valid) }) it('address', () => { const { address, ...data } = model const valid = validate({ ...data, address: { ...address, city: 1234 } }) assert.deepStrictEqual(validate.errors, [ { instancePath: '/address/city', keyword: 'type', message: 'must be string', params: { type: 'string' }, schemaPath: '#address/properties/city/type' } ]) assert.ok(!valid) }) }) }) describe('basic.json', () => { it('generate', () => { const [step] = basic assert.deepStrictEqual( S.array() .title('Product set') .items( S.object() .title('Product') .prop( 'uuid', S.number() .description('The unique identifier for a product') .required() ) .prop('name', S.string()) .required() .prop('price', S.number().exclusiveMinimum(0).required()) .prop( 'tags', S.array().items(S.string()).minItems(1).uniqueItems(true) ) .prop( 'dimensions', S.object() .prop('length', S.number().required()) .prop('width', S.number().required()) .prop('height', S.number().required()) ) .prop( 'warehouseLocation', S.string().description( 'Coordinates of the warehouse with the product' ) ) ) .valueOf(), { ...step.schema, items: { ...step.schema.items, properties: { ...step.schema.items.properties, dimensions: { ...step.schema.items.properties.dimensions, properties: { length: { $id: undefined, type: 'number' }, width: { $id: undefined, type: 'number' }, height: { $id: undefined, type: 'number' } } } } } } ) }) }) describe('raw', () => { describe('swaggger', () => { describe('nullable', () => { it('allows nullable', () => { const ajv = new Ajv() const schema = S.object() .prop('foo', S.raw({ nullable: true, type: 'string' })) .valueOf() const validate = ajv.compile(schema) const valid = validate({ test: null }) assert.strictEqual(validate.errors, null) assert.ok(valid) }) }) }) describe('ajv', () => { describe('formatMaximum', () => { it('checks custom keyword formatMaximum', () => { const ajv = new Ajv() require('ajv-formats')(ajv) /* const schema = S.string() .raw({ nullable: false }) .valueOf() */ // { type: 'number', nullable: true } const schema = S.object() .prop( 'birthday', S.raw({ format: 'date', formatMaximum: '2020-01-01', type: 'string' }) ) .valueOf() const validate = ajv.compile(schema) const valid = validate({ birthday: '2030-01-01' }) assert.deepStrictEqual(validate.errors, [ { instancePath: '/birthday', keyword: 'formatMaximum', message: 'should be <= 2020-01-01', params: { comparison: '<=', limit: '2020-01-01' }, schemaPath: '#/properties/birthday/formatMaximum' } ]) assert.ok(!valid) }) it('checks custom keyword larger with $data', () => { const ajv = new Ajv({ $data: true }) require('ajv-formats')(ajv) /* const schema = S.string() .raw({ nullable: false }) .valueOf() */ // { type: 'number', nullable: true } const schema = S.object() .prop('smaller', S.number().raw({ maximum: { $data: '1/larger' } })) .prop('larger', S.number()) .valueOf() const validate = ajv.compile(schema) const valid = validate({ smaller: 10, larger: 7 }) assert.deepStrictEqual(validate.errors, [ { instancePath: '/smaller', keyword: 'maximum', message: 'must be <= 7', params: { comparison: '<=', limit: 7 }, schemaPath: '#/properties/smaller/maximum' } ]) assert.ok(!valid) }) }) }) }) }) ================================================ FILE: src/FluentSchema.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const S = require('./FluentJSONSchema') describe('S', () => { it('defined', () => { assert.notStrictEqual(S, undefined) }) describe('factory', () => { it('without params', () => { assert.deepStrictEqual(S.object().valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', type: 'object' }) }) describe('generatedIds', () => { describe('properties', () => { it('true', () => { assert.deepStrictEqual( S.withOptions({ generateIds: true }) .object() .prop('prop', S.string()) .valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', properties: { prop: { $id: '#properties/prop', type: 'string' } }, type: 'object' } ) }) it('false', () => { assert.deepStrictEqual( S.object().prop('prop', S.string()).valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', properties: { prop: { type: 'string' } }, type: 'object' } ) }) describe('nested', () => { it('true', () => { assert.deepStrictEqual( S.withOptions({ generateIds: true }) .object() .prop('foo', S.object().prop('bar', S.string()).required()) .valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', properties: { foo: { $id: '#properties/foo', properties: { bar: { $id: '#properties/foo/properties/bar', type: 'string' } }, required: ['bar'], type: 'object' } }, type: 'object' } ) }) it('false', () => { const id = 'myId' assert.deepStrictEqual( S.object() .prop( 'foo', S.object() .prop('bar', S.string().id(id)) .required() ) .valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', properties: { foo: { properties: { bar: { $id: 'myId', type: 'string' } }, required: ['bar'], type: 'object' } }, type: 'object' } ) }) }) }) // TODO LS not sure the test makes sense describe('definitions', () => { it('true', () => { assert.deepStrictEqual( S.withOptions({ generateIds: true }) .object() .definition( 'entity', S.object().prop('foo', S.string()).prop('bar', S.string()) ) .prop('prop') .ref('entity') .valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', definitions: { entity: { $id: '#definitions/entity', properties: { bar: { type: 'string' }, foo: { type: 'string' } }, type: 'object' } }, properties: { prop: { $ref: 'entity' } }, type: 'object' } ) }) it('false', () => { assert.deepStrictEqual( S.withOptions({ generateIds: false }) .object() .definition( 'entity', S.object().id('myCustomId').prop('foo', S.string()) ) .prop('prop') .ref('entity') .valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', definitions: { entity: { $id: 'myCustomId', properties: { foo: { type: 'string' } }, type: 'object' } }, properties: { prop: { $ref: 'entity' } }, type: 'object' } ) }) it('nested', () => { const id = 'myId' assert.deepStrictEqual( S.object() .prop('foo', S.object().prop('bar', S.string().id(id)).required()) .valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', properties: { foo: { properties: { bar: { $id: 'myId', type: 'string' } }, required: ['bar'], type: 'object' } }, type: 'object' } ) }) }) }) }) describe('composition', () => { it('anyOf', () => { const schema = S.object() .prop('foo', S.anyOf([S.string()])) .valueOf() assert.deepStrictEqual(schema, { $schema: 'http://json-schema.org/draft-07/schema#', properties: { foo: { anyOf: [{ type: 'string' }] } }, type: 'object' }) }) it('oneOf', () => { const schema = S.object() .prop( 'multipleRestrictedTypesKey', S.oneOf([S.string(), S.number().minimum(10)]) ) .prop('notTypeKey', S.not(S.oneOf([S.string().pattern('js$')]))) .valueOf() assert.deepStrictEqual(schema, { $schema: 'http://json-schema.org/draft-07/schema#', properties: { multipleRestrictedTypesKey: { oneOf: [{ type: 'string' }, { minimum: 10, type: 'number' }] }, notTypeKey: { not: { oneOf: [{ pattern: 'js$', type: 'string' }] } } }, type: 'object' }) }) }) it('valueOf', () => { assert.deepStrictEqual(S.object().prop('foo', S.string()).valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', properties: { foo: { type: 'string' } }, type: 'object' }) }) it('works', () => { const schema = S.object() .id('http://foo.com/user') .title('A User') .description('A User desc') .definition( 'address', S.object() .id('#address') .prop('country', S.string()) .prop('city', S.string()) .prop('zipcode', S.string()) ) .prop('username', S.string()) .required() .prop('password', S.string()) .required() .prop('address', S.ref('#address')) .required() .prop( 'role', S.object() .id('http://foo.com/role') .prop('name', S.string()) .prop('permissions', S.string()) ) .required() .prop('age', S.number()) .valueOf() assert.deepStrictEqual(schema, { definitions: { address: { type: 'object', $id: '#address', properties: { country: { type: 'string' }, city: { type: 'string' }, zipcode: { type: 'string' } } } }, $schema: 'http://json-schema.org/draft-07/schema#', type: 'object', required: ['username', 'password', 'address', 'role'], $id: 'http://foo.com/user', title: 'A User', description: 'A User desc', properties: { username: { type: 'string' }, password: { type: 'string' }, address: { $ref: '#address' }, age: { type: 'number' }, role: { type: 'object', $id: 'http://foo.com/role', properties: { name: { $id: undefined, type: 'string' }, permissions: { $id: undefined, type: 'string' } } } } }) }) describe('raw', () => { describe('base', () => { it('parses type', () => { const input = S.enum(['foo']).valueOf() const schema = S.raw(input) assert.ok(schema.isFluentSchema) assert.deepStrictEqual(schema.valueOf(), { ...input }) }) it('adds an attribute', () => { const input = S.enum(['foo']).valueOf() const schema = S.raw(input) const attribute = 'title' const modified = schema.title(attribute) assert.ok(schema.isFluentSchema) assert.deepStrictEqual(modified.valueOf(), { ...input, title: attribute }) }) }) describe('string', () => { it('parses type', () => { const input = S.string().valueOf() const schema = S.raw(input) assert.ok(schema.isFluentSchema) assert.deepStrictEqual(schema.valueOf(), { ...input }) }) it('adds an attribute', () => { const input = S.string().valueOf() const schema = S.raw(input) const modified = schema.minLength(3) assert.ok(schema.isFluentSchema) assert.deepStrictEqual(modified.valueOf(), { minLength: 3, ...input }) }) it('parses a prop', () => { const input = S.string().minLength(5).valueOf() const schema = S.raw(input) assert.ok(schema.isFluentSchema) assert.deepStrictEqual(schema.valueOf(), { ...input }) }) }) describe('number', () => { it('parses type', () => { const input = S.number().valueOf() const schema = S.raw(input) assert.ok(schema.isFluentSchema) assert.deepStrictEqual(schema.valueOf(), { ...input }) }) it('adds an attribute', () => { const input = S.number().valueOf() const schema = S.raw(input) const modified = schema.maximum(3) assert.ok(schema.isFluentSchema) assert.deepStrictEqual(modified.valueOf(), { maximum: 3, ...input }) }) it('parses a prop', () => { const input = S.number().maximum(5).valueOf() const schema = S.raw(input) assert.ok(schema.isFluentSchema) assert.deepStrictEqual(schema.valueOf(), { ...input }) }) }) describe('integer', () => { it('parses type', () => { const input = S.integer().valueOf() const schema = S.raw(input) assert.ok(schema.isFluentSchema) assert.deepStrictEqual(schema.valueOf(), { ...input }) }) it('adds an attribute', () => { const input = S.integer().valueOf() const schema = S.raw(input) const modified = schema.maximum(3) assert.ok(schema.isFluentSchema) assert.deepStrictEqual(modified.valueOf(), { maximum: 3, ...input }) }) it('parses a prop', () => { const input = S.integer().maximum(5).valueOf() const schema = S.raw(input) assert.ok(schema.isFluentSchema) assert.deepStrictEqual(schema.valueOf(), { ...input }) }) }) describe('boolean', () => { it('parses type', () => { const input = S.boolean().valueOf() const schema = S.raw(input) assert.ok(schema.isFluentSchema) assert.deepStrictEqual(schema.valueOf(), { ...input }) }) }) describe('object', () => { it('parses type', () => { const input = S.object().valueOf() const schema = S.raw(input) assert.ok(schema.isFluentSchema) assert.deepStrictEqual(schema.valueOf(), { ...input }) }) it('parses properties', () => { const input = S.object().prop('foo').prop('bar', S.string()).valueOf() const schema = S.raw(input) assert.ok(schema.isFluentSchema) assert.deepStrictEqual(schema.valueOf(), { ...input }) }) it('parses nested properties', () => { const input = S.object() .prop('foo', S.object().prop('bar', S.string().minLength(3))) .valueOf() const schema = S.raw(input) const modified = schema.prop('boom') assert.ok(modified.isFluentSchema) assert.deepStrictEqual(modified.valueOf(), { ...input, properties: { ...input.properties, boom: {} } }) }) it('parses definitions', () => { const input = S.object().definition('foo', S.string()).valueOf() const schema = S.raw(input) assert.ok(schema.isFluentSchema) assert.deepStrictEqual(schema.valueOf(), { ...input }) }) }) describe('array', () => { it('parses type', () => { const input = S.array().items(S.string()).valueOf() const schema = S.raw(input) assert.ok(schema.isFluentSchema) assert.deepStrictEqual(schema.valueOf(), { ...input }) }) it('parses properties', () => { const input = S.array().items(S.string()).valueOf() const schema = S.raw(input).maxItems(1) assert.ok(schema.isFluentSchema) assert.deepStrictEqual(schema.valueOf(), { ...input, maxItems: 1 }) }) it('parses nested properties', () => { const input = S.array() .items( S.object().prop( 'foo', S.object().prop('bar', S.string().minLength(3)) ) ) .valueOf() const schema = S.raw(input) const modified = schema.maxItems(1) assert.ok(modified.isFluentSchema) assert.deepStrictEqual(modified.valueOf(), { ...input, maxItems: 1 }) }) it('parses definitions', () => { const input = S.object().definition('foo', S.string()).valueOf() const schema = S.raw(input) assert.ok(schema.isFluentSchema) assert.deepStrictEqual(schema.valueOf(), { ...input }) }) }) }) }) ================================================ FILE: src/IntegerSchema.js ================================================ 'use strict' const { NumberSchema } = require('./NumberSchema') const initialState = { type: 'integer' } /** * Represents a NumberSchema. * @param {Object} [options] - Options * @param {NumberSchema} [options.schema] - Default schema * @param {boolean} [options.generateIds = false] - generate the id automatically e.g. #properties.foo * @returns {NumberSchema} */ // https://medium.com/javascript-scene/javascript-factory-functions-with-es6-4d224591a8b1 // Factory Functions for Mixin Composition withBaseSchema const IntegerSchema = ( { schema, ...options } = { schema: initialState, generateIds: false, factory: IntegerSchema } ) => ({ ...NumberSchema({ ...options, schema }) }) module.exports = { IntegerSchema, default: IntegerSchema } ================================================ FILE: src/IntegerSchema.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const { IntegerSchema } = require('./IntegerSchema') const S = require('./FluentJSONSchema') describe('IntegerSchema', () => { it('defined', () => { assert.notStrictEqual(IntegerSchema, undefined) }) it('Expose symbol', () => { assert.notStrictEqual( IntegerSchema()[Symbol.for('fluent-schema-object')], undefined ) }) describe('constructor', () => { it('without params', () => { assert.deepStrictEqual(IntegerSchema().valueOf(), { type: 'integer' }) }) it('from S', () => { assert.deepStrictEqual(S.integer().valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', type: 'integer' }) }) }) describe('keywords:', () => { describe('minimum', () => { it('valid', () => { const prop = 'prop' assert.deepStrictEqual( S.object().prop(prop, S.integer().minimum(5)).valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', properties: { prop: { type: 'integer', minimum: 5 } }, type: 'object' } ) }) it('invalid number', () => { assert.throws( () => S.integer().minimum('5.1'), (err) => err instanceof S.FluentSchemaError && err.message === "'minimum' must be a Number" ) }) it('invalid integer', () => { assert.throws( () => S.integer().minimum(5.1), (err) => err instanceof S.FluentSchemaError && err.message === "'minimum' must be an Integer" ) }) }) describe('maximum', () => { it('valid', () => { const prop = 'prop' assert.deepStrictEqual( S.object().prop(prop, S.integer().maximum(5)).valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', properties: { prop: { type: 'integer', maximum: 5 } }, type: 'object' } ) }) it('invalid number', () => { assert.throws( () => S.integer().maximum('5.1'), (err) => err instanceof S.FluentSchemaError && err.message === "'maximum' must be a Number" ) }) it('invalid float', () => { assert.throws( () => S.integer().maximum(5.1), (err) => err instanceof S.FluentSchemaError && err.message === "'maximum' must be an Integer" ) }) }) describe('multipleOf', () => { it('valid', () => { const prop = 'prop' assert.deepStrictEqual( S.object().prop(prop, S.integer().multipleOf(5)).valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', properties: { prop: { type: 'integer', multipleOf: 5 } }, type: 'object' } ) }) it('invalid value', () => { assert.throws( () => S.integer().multipleOf('5.1'), (err) => err instanceof S.FluentSchemaError && err.message === "'multipleOf' must be a Number" ) }) it('invalid integer', () => { assert.throws( () => S.integer().multipleOf(5.1), (err) => err instanceof S.FluentSchemaError && err.message === "'multipleOf' must be an Integer" ) }) }) describe('exclusiveMinimum', () => { it('valid', () => { const prop = 'prop' assert.deepStrictEqual( S.object().prop(prop, S.integer().exclusiveMinimum(5)).valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', properties: { prop: { type: 'integer', exclusiveMinimum: 5 } }, type: 'object' } ) }) it('invalid number', () => { assert.throws( () => S.integer().exclusiveMinimum('5.1'), (err) => err instanceof S.FluentSchemaError && err.message === "'exclusiveMinimum' must be a Number" ) }) it('invalid integer', () => { assert.throws( () => S.integer().exclusiveMinimum(5.1), (err) => err instanceof S.FluentSchemaError && err.message === "'exclusiveMinimum' must be an Integer" ) }) }) describe('exclusiveMaximum', () => { it('valid', () => { const prop = 'prop' assert.deepStrictEqual( S.object().prop(prop, S.integer().exclusiveMaximum(5)).valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', properties: { prop: { type: 'integer', exclusiveMaximum: 5 } }, type: 'object' } ) }) it('invalid number', () => { assert.throws( () => S.integer().exclusiveMaximum('5.1'), (err) => err instanceof S.FluentSchemaError && err.message === "'exclusiveMaximum' must be a Number" ) }) it('invalid integer', () => { assert.throws( () => S.integer().exclusiveMaximum(5.1), (err) => err instanceof S.FluentSchemaError && err.message === "'exclusiveMaximum' must be an Integer" ) }) }) }) describe('raw', () => { it('allows to add a custom attribute', () => { const schema = IntegerSchema().raw({ customKeyword: true }).valueOf() assert.deepStrictEqual(schema, { type: 'integer', customKeyword: true }) }) }) it('works', () => { const schema = S.object() .id('http://foo.com/user') .title('A User') .description('A User desc') .prop('age', S.integer().maximum(10)) .valueOf() assert.deepStrictEqual(schema, { $id: 'http://foo.com/user', $schema: 'http://json-schema.org/draft-07/schema#', description: 'A User desc', properties: { age: { maximum: 10, type: 'integer' } }, title: 'A User', type: 'object' }) }) }) ================================================ FILE: src/MixedSchema.js ================================================ 'use strict' const { NullSchema } = require('./NullSchema') const { BooleanSchema } = require('./BooleanSchema') const { StringSchema } = require('./StringSchema') const { NumberSchema } = require('./NumberSchema') const { IntegerSchema } = require('./IntegerSchema') const { ObjectSchema } = require('./ObjectSchema') const { ArraySchema } = require('./ArraySchema') const { TYPES, FLUENT_SCHEMA } = require('./utils') const initialState = { type: [], definitions: [], properties: [], required: [] } /** * Represents a MixedSchema. * @param {Object} [options] - Options * @param {MixedSchema} [options.schema] - Default schema * @param {boolean} [options.generateIds = false] - generate the id automatically e.g. #properties.foo * @returns {StringSchema} */ const MixedSchema = ({ schema = initialState, ...options } = {}) => { options = { generateIds: false, factory: MixedSchema, ...options } return { [FLUENT_SCHEMA]: true, ...(schema.type.includes(TYPES.STRING) ? StringSchema({ ...options, schema, factory: MixedSchema }) : {}), ...(schema.type.includes(TYPES.NUMBER) ? NumberSchema({ ...options, schema, factory: MixedSchema }) : {}), ...(schema.type.includes(TYPES.BOOLEAN) ? BooleanSchema({ ...options, schema, factory: MixedSchema }) : {}), ...(schema.type.includes(TYPES.INTEGER) ? IntegerSchema({ ...options, schema, factory: MixedSchema }) : {}), ...(schema.type.includes(TYPES.OBJECT) ? ObjectSchema({ ...options, schema, factory: MixedSchema }) : {}), ...(schema.type.includes(TYPES.ARRAY) ? ArraySchema({ ...options, schema, factory: MixedSchema }) : {}), ...(schema.type.includes(TYPES.NULL) ? NullSchema({ ...options, schema, factory: MixedSchema }) : {}) } } module.exports = { MixedSchema, default: MixedSchema } ================================================ FILE: src/MixedSchema.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const { MixedSchema } = require('./MixedSchema') const S = require('./FluentJSONSchema') describe('MixedSchema', () => { it('defined', () => { assert.notStrictEqual(MixedSchema, undefined) }) it('Expose symbol / 1', () => { assert.notStrictEqual( MixedSchema()[Symbol.for('fluent-schema-object')], undefined ) }) it('Expose symbol / 2', () => { const types = [ S.TYPES.STRING, S.TYPES.NUMBER, S.TYPES.BOOLEAN, S.TYPES.INTEGER, S.TYPES.OBJECT, S.TYPES.ARRAY, S.TYPES.NULL ] assert.notStrictEqual( MixedSchema(types)[Symbol.for('fluent-schema-object')], undefined ) }) describe('factory', () => { it('without params', () => { assert.deepStrictEqual(MixedSchema().valueOf(), { [Symbol.for('fluent-schema-object')]: true }) }) }) describe('from S', () => { it('valid', () => { const types = [ S.TYPES.STRING, S.TYPES.NUMBER, S.TYPES.BOOLEAN, S.TYPES.INTEGER, S.TYPES.OBJECT, S.TYPES.ARRAY, S.TYPES.NULL ] assert.deepStrictEqual(S.mixed(types).valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', type: types }) }) it('invalid param', () => { const types = '' assert.throws( () => S.mixed(types), (err) => err instanceof S.FluentSchemaError && err.message === "Invalid 'types'. It must be an array of types. Valid types are string | number | boolean | integer | object | array | null" ) }) it('invalid type', () => { const types = ['string', 'invalid'] assert.throws( () => S.mixed(types), (err) => err instanceof S.FluentSchemaError && err.message === "Invalid 'types'. It must be an array of types. Valid types are string | number | boolean | integer | object | array | null" ) }) }) it('sets a type object to the prop', () => { assert.deepStrictEqual( S.object() .prop( 'prop', S.mixed([S.TYPES.STRING, S.TYPES.NUMBER]).minimum(10).maxLength(5) ) .valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', properties: { prop: { maxLength: 5, minimum: 10, type: ['string', 'number'] } }, type: 'object' } ) }) describe('raw', () => { it('allows to add a custom attribute', () => { const types = [S.TYPES.STRING, S.TYPES.NUMBER] const schema = S.mixed(types).raw({ customKeyword: true }).valueOf() assert.deepStrictEqual(schema, { $schema: 'http://json-schema.org/draft-07/schema#', type: ['string', 'number'], customKeyword: true }) }) }) }) ================================================ FILE: src/NullSchema.js ================================================ 'use strict' const { BaseSchema } = require('./BaseSchema') const { setAttribute, FLUENT_SCHEMA } = require('./utils') const initialState = { type: 'null' } /** * Represents a NullSchema. * @param {Object} [options] - Options * @param {StringSchema} [options.schema] - Default schema * @param {boolean} [options.generateIds = false] - generate the id automatically e.g. #properties.foo * @returns {StringSchema} */ const NullSchema = ({ schema = initialState, ...options } = {}) => { options = { generateIds: false, factory: NullSchema, ...options } const { valueOf, raw } = BaseSchema({ ...options, schema }) return { valueOf, raw, [FLUENT_SCHEMA]: true, isFluentSchema: true, /** * Set a property to type null * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.1.1|reference} * @returns {FluentSchema} */ null: () => setAttribute({ schema, ...options }, ['type', 'null']) } } module.exports = { NullSchema, default: NullSchema } ================================================ FILE: src/NullSchema.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const { NullSchema } = require('./NullSchema') const S = require('./FluentJSONSchema') describe('NullSchema', () => { it('defined', () => { assert.notStrictEqual(NullSchema, undefined) }) it('Expose symbol', () => { assert.notStrictEqual( NullSchema()[Symbol.for('fluent-schema-object')], undefined ) }) describe('constructor', () => { it('without params', () => { assert.deepStrictEqual(NullSchema().valueOf(), { type: 'null' }) }) it('from S', () => { assert.deepStrictEqual(S.null().valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', type: 'null' }) }) }) it('sets a null type to the prop', () => { assert.strictEqual( S.object().prop('prop', S.null()).valueOf().properties.prop.type, 'null' ) }) describe('raw', () => { it('allows to add a custom attribute', () => { const schema = NullSchema().raw({ customKeyword: true }).valueOf() assert.deepStrictEqual(schema, { type: 'null', customKeyword: true }) }) }) }) ================================================ FILE: src/NumberSchema.js ================================================ 'use strict' const { BaseSchema } = require('./BaseSchema') const { setAttribute, FluentSchemaError } = require('./utils') const initialState = { type: 'number' } /** * Represents a NumberSchema. * @param {Object} [options] - Options * @param {NumberSchema} [options.schema] - Default schema * @param {boolean} [options.generateIds = false] - generate the id automatically e.g. #properties.foo * @returns {NumberSchema} */ // https://medium.com/javascript-scene/javascript-factory-functions-with-es6-4d224591a8b1 // Factory Functions for Mixin Composition withBaseSchema const NumberSchema = ( { schema, ...options } = { schema: initialState, generateIds: false, factory: NumberSchema } ) => ({ ...BaseSchema({ ...options, schema }), /** * It represents an inclusive lower limit for a numeric instance. * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.2.4|reference} * @param {number} min * @returns {FluentSchema} */ minimum: min => { if (typeof min !== 'number') { throw new FluentSchemaError("'minimum' must be a Number") } if (schema.type === 'integer' && !Number.isInteger(min)) { throw new FluentSchemaError("'minimum' must be an Integer") } return setAttribute({ schema, ...options }, ['minimum', min, 'number']) }, /** * It represents an exclusive lower limit for a numeric instance. * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.2.5|reference} * @param {number} min * @returns {FluentSchema} */ exclusiveMinimum: min => { if (typeof min !== 'number') { throw new FluentSchemaError("'exclusiveMinimum' must be a Number") } if (schema.type === 'integer' && !Number.isInteger(min)) { throw new FluentSchemaError("'exclusiveMinimum' must be an Integer") } return setAttribute({ schema, ...options }, [ 'exclusiveMinimum', min, 'number' ]) }, /** * It represents an inclusive upper limit for a numeric instance. * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.2.2|reference} * @param {number} max * @returns {FluentSchema} */ maximum: max => { if (typeof max !== 'number') { throw new FluentSchemaError("'maximum' must be a Number") } if (schema.type === 'integer' && !Number.isInteger(max)) { throw new FluentSchemaError("'maximum' must be an Integer") } return setAttribute({ schema, ...options }, ['maximum', max, 'number']) }, /** * It represents an exclusive upper limit for a numeric instance. * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.2.3|reference} * @param {number} max * @returns {FluentSchema} */ exclusiveMaximum: max => { if (typeof max !== 'number') { throw new FluentSchemaError("'exclusiveMaximum' must be a Number") } if (schema.type === 'integer' && !Number.isInteger(max)) { throw new FluentSchemaError("'exclusiveMaximum' must be an Integer") } return setAttribute({ schema, ...options }, [ 'exclusiveMaximum', max, 'number' ]) }, /** * It's strictly greater than 0. * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.2.1|reference} * @param {number} multiple * @returns {FluentSchema} */ multipleOf: multiple => { if (typeof multiple !== 'number') { throw new FluentSchemaError("'multipleOf' must be a Number") } if (schema.type === 'integer' && !Number.isInteger(multiple)) { throw new FluentSchemaError("'multipleOf' must be an Integer") } return setAttribute({ schema, ...options }, [ 'multipleOf', multiple, 'number' ]) } }) module.exports = { NumberSchema, default: NumberSchema } ================================================ FILE: src/NumberSchema.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const { NumberSchema } = require('./NumberSchema') const S = require('./FluentJSONSchema') describe('NumberSchema', () => { it('defined', () => { assert.notStrictEqual(NumberSchema, undefined) }) it('Expose symbol', () => { assert.notStrictEqual( NumberSchema()[Symbol.for('fluent-schema-object')], undefined ) }) describe('constructor', () => { it('without params', () => { assert.deepStrictEqual(NumberSchema().valueOf(), { type: 'number' }) }) it('from S', () => { assert.deepStrictEqual(S.number().valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', type: 'number' }) }) }) describe('keywords:', () => { describe('minimum', () => { it('valid', () => { const prop = 'prop' assert.deepStrictEqual( S.object().prop(prop, S.number().minimum(5.1)).valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', properties: { prop: { type: 'number', minimum: 5.1 } }, type: 'object' } ) }) it('invalid value', () => { assert.throws( () => S.number().minimum('5.1'), (err) => err instanceof S.FluentSchemaError && err.message === "'minimum' must be a Number" ) }) }) describe('maximum', () => { it('valid', () => { const prop = 'prop' assert.deepStrictEqual( S.object().prop(prop, S.number().maximum(5.1)).valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', properties: { prop: { type: 'number', maximum: 5.1 } }, type: 'object' } ) }) it('invalid value', () => { assert.throws( () => S.number().maximum('5.1'), (err) => err instanceof S.FluentSchemaError && err.message === "'maximum' must be a Number" ) }) }) describe('multipleOf', () => { it('valid', () => { const prop = 'prop' assert.deepStrictEqual( S.object().prop(prop, S.number().multipleOf(5.1)).valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', properties: { prop: { type: 'number', multipleOf: 5.1 } }, type: 'object' } ) }) it('invalid value', () => { assert.throws( () => S.number().multipleOf('5.1'), (err) => err instanceof S.FluentSchemaError && err.message === "'multipleOf' must be a Number" ) }) }) describe('exclusiveMinimum', () => { it('valid', () => { const prop = 'prop' assert.deepStrictEqual( S.object().prop(prop, S.number().exclusiveMinimum(5.1)).valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', properties: { prop: { type: 'number', exclusiveMinimum: 5.1 } }, type: 'object' } ) }) it('invalid value', () => { assert.throws( () => S.number().exclusiveMinimum('5.1'), (err) => err instanceof S.FluentSchemaError && err.message === "'exclusiveMinimum' must be a Number" ) }) }) describe('exclusiveMaximum', () => { it('valid', () => { const prop = 'prop' assert.deepStrictEqual( S.object().prop(prop, S.number().exclusiveMaximum(5.1)).valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', properties: { prop: { type: 'number', exclusiveMaximum: 5.1 } }, type: 'object' } ) }) it('invalid value', () => { assert.throws( () => S.number().exclusiveMaximum('5.1'), (err) => err instanceof S.FluentSchemaError && err.message === "'exclusiveMaximum' must be a Number" ) }) }) describe('raw', () => { it('allows to add a custom attribute', () => { const schema = NumberSchema().raw({ customKeyword: true }).valueOf() assert.deepStrictEqual(schema, { type: 'number', customKeyword: true }) }) }) }) it('works', () => { const schema = S.object() .id('http://foo.com/user') .title('A User') .description('A User desc') .prop('age', S.number().maximum(10)) .valueOf() assert.deepStrictEqual(schema, { $id: 'http://foo.com/user', $schema: 'http://json-schema.org/draft-07/schema#', description: 'A User desc', properties: { age: { maximum: 10, type: 'number' } }, title: 'A User', type: 'object' }) }) }) ================================================ FILE: src/ObjectSchema.js ================================================ 'use strict' const { BaseSchema } = require('./BaseSchema') const { omit, setAttribute, isFluentSchema, hasCombiningKeywords, patchIdsWithParentId, appendRequired, FluentSchemaError, combineDeepmerge } = require('./utils') const initialState = { type: 'object', definitions: [], properties: [], required: [] } /** * Represents a ObjectSchema. * @param {Object} [options] - Options * @param {StringSchema} [options.schema] - Default schema * @param {boolean} [options.generateIds = false] - generate the id automatically e.g. #properties.foo * @returns {StringSchema} */ const ObjectSchema = ({ schema = initialState, ...options } = {}) => { // TODO LS think about default values and how pass all of them through the functions options = { generateIds: false, factory: ObjectSchema, ...options } return { ...BaseSchema({ ...options, schema }), /** * It defines a URI for the schema, and the base URI that other URI references within the schema are resolved against. * Calling `id` on an ObjectSchema will alway set the id on the root of the object rather than in its "properties", which * differs from other schema types. * * {@link https://tools.ietf.org/html/draft-handrews-json-schema-01#section-8.2|reference} * @param {string} id - an #id **/ id: id => { if (!id) { throw new FluentSchemaError( 'id should not be an empty fragment <#> or an empty string <> (e.g. #myId)' ) } return options.factory({ schema: { ...schema, $id: id }, ...options }) }, /** * This keyword determines how child instances validate for objects, and does not directly validate the immediate instance itself. * Validation with "additionalProperties" applies only to the child values of instance names that do not match any names in "properties", * and do not match any regular expression in "patternProperties". * For all such properties, validation succeeds if the child instance validates against the "additionalProperties" schema. * Omitting this keyword has the same behavior as an empty schema. * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.5.6|reference} * @param {FluentSchema|boolean} value * @returns {FluentSchema} */ additionalProperties: value => { if (typeof value === 'boolean') { return setAttribute({ schema, ...options }, [ 'additionalProperties', value, 'object' ]) } if (isFluentSchema(value)) { const { $schema, ...rest } = value.valueOf({ isRoot: false }) return setAttribute({ schema, ...options }, [ 'additionalProperties', { ...rest }, 'array' ]) } throw new FluentSchemaError( "'additionalProperties' must be a boolean or a S" ) }, /** * An object instance is valid against "maxProperties" if its number of properties is less than, or equal to, the value of this keyword. * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.5.1|reference} * @param {number} max * @returns {FluentSchema} */ maxProperties: max => { if (!Number.isInteger(max)) { throw new FluentSchemaError("'maxProperties' must be a Integer") } return setAttribute({ schema, ...options }, [ 'maxProperties', max, 'object' ]) }, /** * An object instance is valid against "minProperties" if its number of properties is greater than, or equal to, the value of this keyword. * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.5.2|reference} * @param {number} min * @returns {FluentSchema} */ minProperties: min => { if (!Number.isInteger(min)) { throw new FluentSchemaError("'minProperties' must be a Integer") } return setAttribute({ schema, ...options }, [ 'minProperties', min, 'object' ]) }, /** * Each property name of this object SHOULD be a valid regular expression, according to the ECMA 262 regular expression dialect. * Each property value of this object MUST be a valid JSON Schema. * This keyword determines how child instances validate for objects, and does not directly validate the immediate instance itself. * Validation of the primitive instance type against this keyword always succeeds. * Validation succeeds if, for each instance name that matches any regular expressions that appear as a property name in this keyword's value, the child instance for that name successfully validates against each schema that corresponds to a matching regular expression. * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.5.5|reference} * @param {object} opts * @returns {FluentSchema} */ patternProperties: opts => { const values = Object.entries(opts).reduce((memo, [pattern, schema]) => { if (!isFluentSchema(schema)) { throw new FluentSchemaError( "'patternProperties' invalid options. Provide a valid map e.g. { '^fo.*$': S.string() }" ) } memo[pattern] = omit(schema.valueOf({ isRoot: false }), ['$schema']) return memo }, {}) return setAttribute({ schema, ...options }, [ 'patternProperties', values, 'object' ]) }, /** * This keyword specifies rules that are evaluated if the instance is an object and contains a certain property. * This keyword's value MUST be an object. Each property specifies a dependency. Each dependency value MUST be an array or a valid JSON Schema. * If the dependency value is a subschema, and the dependency key is a property in the instance, the entire instance must validate against the dependency value. * If the dependency value is an array, each element in the array, if any, MUST be a string, and MUST be unique. If the dependency key is a property in the instance, each of the items in the dependency value must be a property that exists in the instance. * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.5.7|reference} * @param {object} opts * @returns {FluentSchema} */ dependencies: opts => { const values = Object.entries(opts).reduce((memo, [prop, schema]) => { if (!isFluentSchema(schema) && !Array.isArray(schema)) { throw new FluentSchemaError( "'dependencies' invalid options. Provide a valid map e.g. { 'foo': ['bar'] } or { 'foo': S.string() }" ) } memo[prop] = Array.isArray(schema) ? schema : omit(schema.valueOf({ isRoot: false }), ['$schema', 'type', 'definitions']) return memo }, {}) return setAttribute({ schema, ...options }, [ 'dependencies', values, 'object' ]) }, /** * The value of "properties" MUST be an object. Each dependency value MUST be an array. * Each element in the array MUST be a string and MUST be unique. If the dependency key is a property in the instance, each of the items in the dependency value must be a property that exists in the instance. * * {@link https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.6.5.4|reference} * @param {object} opts * @returns {FluentSchema} */ dependentRequired: opts => { const values = Object.entries(opts).reduce((memo, [prop, schema]) => { if (!Array.isArray(schema)) { throw new FluentSchemaError( "'dependentRequired' invalid options. Provide a valid array e.g. { 'foo': ['bar'] }" ) } memo[prop] = schema return memo }, {}) return setAttribute({ schema, ...options }, [ 'dependentRequired', values, 'object' ]) }, /** * The value of "properties" MUST be an object. The dependency value MUST be a valid JSON Schema. * Each dependency key is a property in the instance and the entire instance must validate against the dependency value. * * {@link https://json-schema.org/draft/2019-09/json-schema-core.html#rfc.section.9.2.2.4|reference} * @param {object} opts * @returns {FluentSchema} */ dependentSchemas: opts => { const values = Object.entries(opts).reduce((memo, [prop, schema]) => { if (!isFluentSchema(schema)) { throw new FluentSchemaError( "'dependentSchemas' invalid options. Provide a valid schema e.g. { 'foo': S.string() }" ) } memo[prop] = omit(schema.valueOf({ isRoot: false }), ['$schema', 'type', 'definitions']) return memo }, {}) return setAttribute({ schema, ...options }, [ 'dependentSchemas', values, 'object' ]) }, /** * If the instance is an object, this keyword validates if every property name in the instance validates against the provided schema. * Note the property name that the schema is testing will always be a string. * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.5.8|reference} * @param {FluentSchema} value * @returns {FluentSchema} */ propertyNames: value => { if (!isFluentSchema(value)) { throw new FluentSchemaError("'propertyNames' must be a S") } return setAttribute({ schema, ...options }, [ 'propertyNames', omit(value.valueOf({ isRoot: false }), ['$schema']), 'object' ]) }, /** * The value of "properties" MUST be an object. Each value of this object MUST be a valid JSON Schema. * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.5.4|reference} * @param {string} name * @param {FluentSchema} props * @returns {FluentSchema} */ prop: (name, props = {}) => { if (Array.isArray(props) || typeof props !== 'object') { throw new FluentSchemaError( `'${name}' doesn't support value '${JSON.stringify( props )}'. Pass a FluentSchema object` ) } const target = props.def ? 'definitions' : 'properties' let attributes = props.valueOf({ isRoot: false }) const { $ref, $id: attributeId, required, ...restAttributes } = attributes const $id = attributeId || (options.generateIds ? `#${target}/${name}` : undefined) if (isFluentSchema(props)) { attributes = patchIdsWithParentId({ schema: attributes, parentId: $id, ...options }) const [schemaPatched, attributesPatched] = appendRequired({ schema, attributes: { ...attributes, name } }) schema = schemaPatched attributes = attributesPatched } const type = hasCombiningKeywords(attributes) ? undefined : attributes.type // strip undefined values or empty arrays or internals attributes = Object.entries({ ...attributes, $id, type }).reduce( (memo, [key, value]) => { if ( key !== '$schema' && key !== 'def' && value !== undefined && !(Array.isArray(value) && value.length === 0 && key !== 'default') ) { memo[key] = value } return memo }, {} ) return ObjectSchema({ schema: { ...schema, [target]: [ ...schema[target], $ref ? { name, $ref, ...restAttributes } : { name, ...attributes } ] }, ...options }) }, extend: base => { if (!base) { throw new FluentSchemaError("Schema can't be null or undefined") } if (!base.isFluentSchema) { throw new FluentSchemaError("Schema isn't FluentSchema type") } const src = base._getState() const extended = combineDeepmerge(src, schema) const { valueOf, isFluentSchema, FLUENT_SCHEMA, _getState, extend } = ObjectSchema({ schema: extended, ...options }) return { valueOf, isFluentSchema, FLUENT_SCHEMA, _getState, extend } }, /** * Returns an object schema with only a subset of keys provided. If called on an ObjectSchema with an * `$id`, it will be removed and the return value will be considered a new schema. * * @param properties a list of properties you want to keep * @returns {ObjectSchema} */ only: properties => { return ObjectSchema({ schema: { ...omit(schema, ['$id', 'properties']), properties: schema.properties.filter(({ name }) => properties.includes(name)), required: schema.required.filter(p => properties.includes(p)) }, ...options }) }, /** * Returns an object schema without a subset of keys provided. If called on an ObjectSchema with an * `$id`, it will be removed and the return value will be considered a new schema. * * @param properties a list of properties you dont want to keep * @returns {ObjectSchema} */ without: properties => { return ObjectSchema({ schema: { ...omit(schema, ['$id', 'properties']), properties: schema.properties.filter(({ name }) => !properties.includes(name)), required: schema.required.filter(p => !properties.includes(p)) }, ...options }) }, /** * The "definitions" keywords provides a standardized location for schema authors to inline re-usable JSON Schemas into a more general schema. * There are no restrictions placed on the values within the array. * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.9|reference} * @param {string} name * @param {FluentSchema} props * @returns {FluentSchema} */ // FIXME LS move to BaseSchema and remove .prop // TODO LS Is a definition a proper schema? definition: (name, props = {}) => ObjectSchema({ schema, ...options }).prop(name, { ...props.valueOf({ isRoot: false }), def: true }) } } module.exports = { ObjectSchema, default: ObjectSchema } ================================================ FILE: src/ObjectSchema.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const { ObjectSchema } = require('./ObjectSchema') const S = require('./FluentJSONSchema') describe('ObjectSchema', () => { it('defined', () => { assert.notStrictEqual(ObjectSchema, undefined) }) it('Expose symbol', () => { assert.notStrictEqual( ObjectSchema()[Symbol.for('fluent-schema-object')], undefined ) }) describe('constructor', () => { it('without params', () => { assert.deepStrictEqual(ObjectSchema().valueOf(), { // $schema: 'http://json-schema.org/draft-07/schema#', type: 'object' }) }) describe('generatedIds', () => { describe('properties', () => { it('true', () => { assert.deepStrictEqual( ObjectSchema({ generateIds: true }) .prop('prop', S.string()) .valueOf(), { properties: { prop: { $id: '#properties/prop', type: 'string' } }, type: 'object' } ) }) it('false', () => { assert.deepStrictEqual(ObjectSchema().prop('prop').valueOf(), { properties: { prop: {} }, type: 'object' }) }) describe('nested', () => { it('true', () => { assert.deepStrictEqual( ObjectSchema({ generateIds: true }) .prop('foo', ObjectSchema().prop('bar').required()) .valueOf(), { properties: { foo: { $id: '#properties/foo', properties: { bar: { $id: '#properties/foo/properties/bar' } }, required: ['bar'], type: 'object' } }, type: 'object' } ) }) it('false', () => { const id = 'myId' assert.deepStrictEqual( ObjectSchema() .prop('foo', ObjectSchema().prop('bar', S.id(id)).required()) .valueOf(), { properties: { foo: { properties: { bar: { $id: 'myId' } }, required: ['bar'], type: 'object' } }, type: 'object' } ) }) it('invalid', () => { assert.throws( () => ObjectSchema().id(''), (err) => err instanceof S.FluentSchemaError && err.message === 'id should not be an empty fragment <#> or an empty string <> (e.g. #myId)' ) }) }) }) describe('definitions', () => { it('true', () => { assert.deepStrictEqual( ObjectSchema({ generateIds: true }) .definition('entity', ObjectSchema().prop('foo').prop('bar')) .prop('prop', S.ref('entity')) .valueOf(), { definitions: { entity: { $id: '#definitions/entity', properties: { bar: {}, foo: {} }, type: 'object' } }, properties: { prop: { $ref: 'entity' } }, type: 'object' } ) }) it('false', () => { assert.deepStrictEqual( ObjectSchema({ generateIds: false }) .definition('entity', ObjectSchema().id('myCustomId').prop('foo')) .prop('prop', S.ref('entity')) .valueOf(), { definitions: { entity: { $id: 'myCustomId', properties: { foo: {} }, type: 'object' } }, properties: { prop: { $ref: 'entity' } }, type: 'object' } ) }) it('nested', () => { const id = 'myId' assert.deepStrictEqual( ObjectSchema() .prop( 'foo', ObjectSchema().prop('bar', S.string().id(id)).required() ) .valueOf(), { properties: { foo: { properties: { bar: { $id: 'myId', type: 'string' } }, required: ['bar'], type: 'object' } }, type: 'object' } ) }) }) }) }) it('from S', () => { assert.deepStrictEqual(S.object().valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', type: 'object' }) }) it('sets a type object to the prop', () => { assert.strictEqual( ObjectSchema().prop('prop', S.object()).valueOf().properties.prop.type, 'object' ) }) it('valueOf', () => { assert.deepStrictEqual(ObjectSchema().prop('foo', S.string()).valueOf(), { properties: { foo: { type: 'string' } }, type: 'object' }) }) describe('keywords:', () => { describe('id', () => { it('valid', () => { const id = 'myId' assert.deepStrictEqual(ObjectSchema().prop('prop').id(id).valueOf(), { $id: id, properties: { prop: {} }, type: 'object' }) }) describe('nested', () => { it('object', () => { const id = 'myId' assert.deepStrictEqual( ObjectSchema().prop('foo', S.string().id(id)).valueOf().properties .foo, { type: 'string', $id: id } ) }) it('string', () => { assert.deepStrictEqual( ObjectSchema().prop('foo', S.string().title('Foo')).valueOf() .properties, { foo: { type: 'string', title: 'Foo' } } ) }) }) }) describe('properties', () => { it('string', () => { assert.deepStrictEqual( ObjectSchema().prop('prop', S.string()).valueOf().properties, { prop: { type: 'string' } } ) }) describe('nested', () => { it('object', () => { assert.deepStrictEqual( ObjectSchema().prop('foo', ObjectSchema().prop('bar')).valueOf() .properties.foo.properties, { bar: { $id: undefined } } ) }) it('string', () => { assert.deepStrictEqual( ObjectSchema().prop('foo', S.string().title('Foo')).valueOf() .properties, { foo: { type: 'string', title: 'Foo' } } ) }) }) describe('invalid', () => { it('throws an error passing a string as value', () => { assert.throws( () => ObjectSchema().prop('prop', 'invalid'), (err) => err instanceof S.FluentSchemaError && err.message === "'prop' doesn't support value '\"invalid\"'. Pass a FluentSchema object" ) }) it('throws an error passing a number as value', () => { assert.throws( () => ObjectSchema().prop('prop', 555), (err) => err instanceof S.FluentSchemaError && err.message === "'prop' doesn't support value '555'. Pass a FluentSchema object" ) }) it('throws an error passing an array as value', () => { assert.throws( () => ObjectSchema().prop('prop', []), (err) => err instanceof S.FluentSchemaError && err.message === "'prop' doesn't support value '[]'. Pass a FluentSchema object" ) }) }) }) describe('additionalProperties', () => { it('true', () => { const value = true assert.deepStrictEqual( ObjectSchema().additionalProperties(value).prop('prop').valueOf() .additionalProperties, value ) }) it('false', () => { const value = false assert.deepStrictEqual( ObjectSchema().additionalProperties(value).prop('prop').valueOf() .additionalProperties, value ) }) it('object', () => { assert.deepStrictEqual( ObjectSchema().additionalProperties(S.string()).prop('prop').valueOf() .additionalProperties, { type: 'string' } ) }) it('invalid', () => { const value = 'invalid' assert.throws( () => assert.strictEqual( ObjectSchema().prop('prop').additionalProperties(value), value ), (err) => err instanceof S.FluentSchemaError && err.message === "'additionalProperties' must be a boolean or a S" ) }) }) describe('maxProperties', () => { it('valid', () => { const value = 2 assert.deepStrictEqual( ObjectSchema().maxProperties(value).prop('prop').valueOf() .maxProperties, value ) }) it('invalid', () => { const value = 'invalid' assert.throws( () => assert.strictEqual( ObjectSchema().prop('prop').maxProperties(value), value ), (err) => err instanceof S.FluentSchemaError && err.message === "'maxProperties' must be a Integer" ) }) }) describe('minProperties', () => { it('valid', () => { const value = 2 assert.deepStrictEqual( ObjectSchema().minProperties(value).prop('prop').valueOf() .minProperties, value ) }) it('invalid', () => { const value = 'invalid' assert.throws( () => assert.strictEqual( ObjectSchema().prop('prop').minProperties(value), value ), (err) => err instanceof S.FluentSchemaError && err.message === "'minProperties' must be a Integer" ) }) }) describe('patternProperties', () => { it('valid', () => { assert.deepStrictEqual( ObjectSchema() .patternProperties({ '^fo.*$': S.string() }) .prop('foo') .valueOf(), { patternProperties: { '^fo.*$': { type: 'string' } }, properties: { foo: {} }, type: 'object' } ) }) it('invalid', () => { const value = 'invalid' assert.throws( () => assert.strictEqual( ObjectSchema().prop('prop').patternProperties(value), value ), (err) => err instanceof S.FluentSchemaError && err.message === "'patternProperties' invalid options. Provide a valid map e.g. { '^fo.*$': S.string() }" ) }) }) describe('dependencies', () => { it('map of array', () => { assert.deepStrictEqual( ObjectSchema() .dependencies({ foo: ['bar'] }) .prop('foo') .prop('bar') .valueOf(), { dependencies: { foo: ['bar'] }, properties: { bar: {}, foo: {} }, type: 'object' } ) }) it('object', () => { assert.deepStrictEqual( ObjectSchema() .dependencies({ foo: ObjectSchema().prop('bar', S.string()) }) .prop('foo') .valueOf(), { dependencies: { foo: { properties: { bar: { type: 'string' } } } }, properties: { foo: {} }, type: 'object' } ) }) it('invalid', () => { const value = 'invalid' assert.throws( () => assert.strictEqual( ObjectSchema().prop('prop').dependencies(value), value ), (err) => err instanceof S.FluentSchemaError && err.message === "'dependencies' invalid options. Provide a valid map e.g. { 'foo': ['bar'] } or { 'foo': S.string() }" ) }) }) describe('dependentRequired', () => { it('valid', () => { assert.deepStrictEqual( ObjectSchema() .dependentRequired({ foo: ['bar'] }) .prop('foo') .prop('bar') .valueOf(), { type: 'object', dependentRequired: { foo: ['bar'] }, properties: { foo: {}, bar: {} } } ) }) it('invalid', () => { const value = { foo: ObjectSchema().prop('bar', S.string()) } assert.throws( () => { assert.deepStrictEqual( ObjectSchema().dependentRequired(value).prop('foo'), value ) }, (err) => err instanceof S.FluentSchemaError && err.message === "'dependentRequired' invalid options. Provide a valid array e.g. { 'foo': ['bar'] }" ) }) }) describe('dependentSchemas', () => { it('valid', () => { assert.deepStrictEqual( ObjectSchema() .dependentSchemas({ foo: ObjectSchema().prop('bar', S.string()) }) .prop('foo') .valueOf(), { dependentSchemas: { foo: { properties: { bar: { type: 'string' } } } }, properties: { foo: {} }, type: 'object' } ) }) it('invalid', () => { const value = { foo: ['bar'] } assert.throws( () => { assert.deepStrictEqual( ObjectSchema().dependentSchemas(value).prop('foo'), value ) }, (err) => err instanceof S.FluentSchemaError && err.message === "'dependentSchemas' invalid options. Provide a valid schema e.g. { 'foo': S.string() }" ) }) }) describe('propertyNames', () => { it('valid', () => { assert.deepStrictEqual( ObjectSchema() .propertyNames(S.string().format(S.FORMATS.EMAIL)) .prop('foo@bar.com') .valueOf().propertyNames, { format: 'email', type: 'string' } ) }) it('invalid', () => { const value = 'invalid' assert.throws( () => assert.strictEqual( ObjectSchema().prop('prop').propertyNames(value), value ), (err) => err instanceof S.FluentSchemaError && err.message === "'propertyNames' must be a S" ) }) }) }) describe('null', () => { it('sets a type object from the root', () => { assert.strictEqual(S.null().valueOf().type, 'null') }) it('sets a type object from the prop', () => { assert.strictEqual( ObjectSchema().prop('value', S.null()).valueOf().properties.value.type, 'null' ) }) }) describe('definition', () => { it('add', () => { assert.deepStrictEqual( ObjectSchema() .definition('foo', ObjectSchema().prop('foo').prop('bar')) .valueOf().definitions, { foo: { type: 'object', properties: { foo: {}, bar: {} } } } ) }) it('empty props', () => { assert.deepStrictEqual( ObjectSchema().definition('foo').valueOf().definitions, { foo: {} } ) }) it('with id', () => { assert.deepStrictEqual( ObjectSchema() .definition( 'foo', ObjectSchema().id('myDefId').prop('foo').prop('bar') ) .valueOf().definitions, { foo: { $id: 'myDefId', type: 'object', properties: { foo: {}, bar: {} } } } ) }) }) describe('extend', () => { it('extends a simple schema', () => { const base = S.object() .id('base') .title('base') .additionalProperties(false) .prop('foo', S.string().minLength(5).required(true)) const extended = S.object() .id('extended') .title('extended') .prop('bar', S.string().required()) .extend(base) assert.deepStrictEqual(extended.valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', $id: 'extended', title: 'extended', additionalProperties: false, properties: { foo: { type: 'string', minLength: 5 }, bar: { type: 'string' } }, required: ['foo', 'bar'], type: 'object' }) }) it('extends a nested schema', () => { const base = S.object() .id('base') .additionalProperties(false) .prop( 'foo', S.object().prop('id', S.string().format('uuid').required()) ) .prop('str', S.string().required()) .prop('bol', S.boolean().required()) .prop('num', S.integer().required()) const extended = S.object() .id('extended') .prop('bar', S.number()) .extend(base) assert.deepStrictEqual(extended.valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', $id: 'extended', additionalProperties: false, properties: { foo: { type: 'object', properties: { id: { $id: undefined, type: 'string', format: 'uuid' } }, required: ['id'] }, bar: { type: 'number' }, str: { type: 'string' }, bol: { type: 'boolean' }, num: { type: 'integer' } }, required: ['str', 'bol', 'num'], type: 'object' }) }) it('extends a schema with definitions', () => { const base = S.object() .id('base') .additionalProperties(false) .definition('def1', S.object().prop('some')) .definition('def2', S.object().prop('somethingElse')) .prop( 'foo', S.object().prop('id', S.string().format('uuid').required()) ) .prop('str', S.string().required()) .prop('bol', S.boolean().required()) .prop('num', S.integer().required()) const extended = S.object() .id('extended') .definition('def1', S.object().prop('someExtended')) .prop('bar', S.number()) .extend(base) assert.deepStrictEqual(extended.valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', definitions: { def1: { type: 'object', properties: { some: {}, someExtended: {} } }, def2: { type: 'object', properties: { somethingElse: {} } } }, type: 'object', $id: 'extended', additionalProperties: false, properties: { foo: { type: 'object', properties: { id: { $id: undefined, type: 'string', format: 'uuid' } }, required: ['id'] }, str: { type: 'string' }, bol: { type: 'boolean' }, num: { type: 'integer' }, bar: { type: 'number' } }, required: ['str', 'bol', 'num'] }) }) it('extends a schema overriding the props', () => { const base = S.object().prop('reason', S.string().title('title')) const extended = S.object() .prop('other') .prop('reason', S.string().minLength(1)) .extend(base) assert.deepStrictEqual(extended.valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', type: 'object', properties: { other: {}, reason: { title: 'title', type: 'string', minLength: 1 } } }) }) it('extends a chain of schemas overriding the props', () => { const base = S.object().prop('reason', S.string().title('title')) const extended = S.object() .prop('other') .prop('reason', S.string().minLength(1)) .extend(base) const extendedAgain = S.object() .prop('again') .prop('reason', S.string().minLength(2)) .extend(extended) .extend(S.object().prop('multiple')) assert.deepStrictEqual(extendedAgain.valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', type: 'object', properties: { other: {}, again: {}, multiple: {}, reason: { title: 'title', type: 'string', minLength: 2 } } }) }) it('throws an error if a schema is not provided', () => { assert.throws( () => S.object().extend(), (err) => err instanceof S.FluentSchemaError && err.message === "Schema can't be null or undefined" ) }) it('throws an error if a schema is invalid', () => { assert.throws( () => S.object().extend('boom!'), (err) => err instanceof S.FluentSchemaError && err.message === "Schema isn't FluentSchema type" ) }) it('throws an error if you append a new prop after extend', () => { assert.throws( () => { const base = S.object() S.object().extend(base).prop('foo') }, (err) => // err instanceof S.FluentSchemaError && err instanceof TypeError && err.message === 'S.object(...).extend(...).prop is not a function' ) }) }) describe('only', () => { it('returns a subset of the object', () => { const base = S.object() .id('base') .title('base') .prop('foo', S.string()) .prop('bar', S.string()) .prop('baz', S.string()) .prop( 'children', S.object().prop('alpha', S.string()).prop('beta', S.string()) ) const only = base.only(['foo']) assert.deepStrictEqual(only.valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', title: 'base', properties: { foo: { type: 'string' } }, type: 'object' }) }) it('works correctly with required properties', () => { const base = S.object() .id('base') .title('base') .prop('foo', S.string().required()) .prop('bar', S.string()) .prop('baz', S.string().required()) .prop('qux', S.string()) const only = base.only(['foo', 'bar']) assert.deepStrictEqual(only.valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', title: 'base', properties: { foo: { type: 'string' }, bar: { type: 'string' } }, required: ['foo'], type: 'object' }) }) }) describe('without', () => { it('returns a subset of the object', () => { const base = S.object() .id('base') .title('base') .prop('foo', S.string()) .prop('bar', S.string()) .prop('baz', S.string()) .prop( 'children', S.object().prop('alpha', S.string()).prop('beta', S.string()) ) const without = base.without(['foo', 'children']) assert.deepStrictEqual(without.valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', title: 'base', properties: { bar: { type: 'string' }, baz: { type: 'string' } }, type: 'object' }) }) it('works correctly with required properties', () => { const base = S.object() .id('base') .title('base') .prop('foo', S.string().required()) .prop('bar', S.string()) .prop('baz', S.string().required()) .prop('qux', S.string()) const without = base.without(['foo', 'bar']) assert.deepStrictEqual(without.valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', title: 'base', properties: { baz: { type: 'string' }, qux: { type: 'string' } }, required: ['baz'], type: 'object' }) }) }) describe('raw', () => { it('allows to add a custom attribute', () => { const schema = ObjectSchema().raw({ customKeyword: true }).valueOf() assert.deepStrictEqual(schema, { type: 'object', customKeyword: true }) }) it('Carry raw properties', () => { const schema = S.object() .prop('test', S.ref('foo').raw({ test: true })) .valueOf() assert.deepStrictEqual(schema, { $schema: 'http://json-schema.org/draft-07/schema#', type: 'object', properties: { test: { $ref: 'foo', test: true } } }) }) it('Carry raw properties multiple props', () => { const schema = S.object() .prop('a', S.string()) .prop('test', S.ref('foo').raw({ test: true })) .valueOf() assert.deepStrictEqual(schema, { $schema: 'http://json-schema.org/draft-07/schema#', type: 'object', properties: { a: { type: 'string' }, test: { $ref: 'foo', test: true } } }) }) }) }) ================================================ FILE: src/RawSchema.js ================================================ 'use strict' const { BaseSchema } = require('./BaseSchema') const { BooleanSchema } = require('./BooleanSchema') const { StringSchema } = require('./StringSchema') const { NumberSchema } = require('./NumberSchema') const { IntegerSchema } = require('./IntegerSchema') const { ObjectSchema } = require('./ObjectSchema') const { ArraySchema } = require('./ArraySchema') const { toArray, FluentSchemaError } = require('./utils') /** * Represents a raw JSON Schema that will be parsed * @param {Object} schema * @returns {FluentSchema} */ const RawSchema = (schema = {}) => { if (typeof schema !== 'object') { throw new FluentSchemaError('A fragment must be a JSON object') } const { type, definitions, properties, required, ...props } = schema switch (schema.type) { case 'string': { const schema = { type, ...props } return StringSchema({ schema, factory: StringSchema }) } case 'integer': { const schema = { type, ...props } return IntegerSchema({ schema, factory: NumberSchema }) } case 'number': { const schema = { type, ...props } return NumberSchema({ schema, factory: NumberSchema }) } case 'boolean': { const schema = { type, ...props } return BooleanSchema({ schema, factory: BooleanSchema }) } case 'object': { const schema = { type, definitions: toArray(definitions) || [], properties: toArray(properties) || [], required: required || [], ...props } return ObjectSchema({ schema, factory: ObjectSchema }) } case 'array': { const schema = { type, ...props } return ArraySchema({ schema, factory: ArraySchema }) } default: { const schema = { ...props } return BaseSchema({ schema, factory: BaseSchema }) } } } module.exports = { RawSchema, default: RawSchema } ================================================ FILE: src/RawSchema.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const { RawSchema } = require('./RawSchema') const S = require('./FluentJSONSchema') describe('RawSchema', () => { it('defined', () => { assert.notStrictEqual(RawSchema, undefined) }) it('Expose symbol', () => { assert.notStrictEqual( RawSchema()[Symbol.for('fluent-schema-object')], undefined ) }) describe('base', () => { it('parses type', () => { const input = S.enum(['foo']).valueOf() const schema = RawSchema(input) assert.ok(schema.isFluentSchema) assert.deepStrictEqual(schema.valueOf(), { ...input }) }) it('adds an attribute', () => { const input = S.enum(['foo']).valueOf() const schema = RawSchema(input) const attribute = 'title' const modified = schema.title(attribute) assert.ok(schema.isFluentSchema) assert.deepStrictEqual(modified.valueOf(), { ...input, title: attribute }) }) it("throws an exception if the input isn't an object", () => { assert.throws( () => RawSchema('boom!'), (err) => err instanceof S.FluentSchemaError && err.message === 'A fragment must be a JSON object' ) }) }) describe('string', () => { it('parses type', () => { const input = S.string().valueOf() const schema = RawSchema(input) assert.ok(schema.isFluentSchema) assert.deepStrictEqual(schema.valueOf(), { ...input }) }) it('adds an attribute', () => { const input = S.string().valueOf() const schema = RawSchema(input) const modified = schema.minLength(3) assert.ok(schema.isFluentSchema) assert.deepStrictEqual(modified.valueOf(), { minLength: 3, ...input }) }) it('parses a prop', () => { const input = S.string().minLength(5).valueOf() const schema = RawSchema(input) assert.ok(schema.isFluentSchema) assert.deepStrictEqual(schema.valueOf(), { ...input }) }) }) describe('number', () => { it('parses type', () => { const input = S.number().valueOf() const schema = RawSchema(input) assert.ok(schema.isFluentSchema) assert.deepStrictEqual(schema.valueOf(), { ...input }) }) it('adds an attribute', () => { const input = S.number().valueOf() const schema = RawSchema(input) const modified = schema.maximum(3) assert.ok(schema.isFluentSchema) assert.deepStrictEqual(modified.valueOf(), { maximum: 3, ...input }) }) it('parses a prop', () => { const input = S.number().maximum(5).valueOf() const schema = RawSchema(input) assert.ok(schema.isFluentSchema) assert.deepStrictEqual(schema.valueOf(), { ...input }) }) }) describe('integer', () => { it('parses type', () => { const input = S.integer().valueOf() const schema = RawSchema(input) assert.ok(schema.isFluentSchema) assert.deepStrictEqual(schema.valueOf(), { ...input }) }) it('adds an attribute', () => { const input = S.integer().valueOf() const schema = RawSchema(input) const modified = schema.maximum(3) assert.ok(schema.isFluentSchema) assert.deepStrictEqual(modified.valueOf(), { maximum: 3, ...input }) }) it('parses a prop', () => { const input = S.integer().maximum(5).valueOf() const schema = RawSchema(input) assert.ok(schema.isFluentSchema) assert.deepStrictEqual(schema.valueOf(), { ...input }) }) }) describe('boolean', () => { it('parses type', () => { const input = S.boolean().valueOf() const schema = RawSchema(input) assert.ok(schema.isFluentSchema) assert.deepStrictEqual(schema.valueOf(), { ...input }) }) }) describe('object', () => { it('parses type', () => { const input = S.object().valueOf() const schema = RawSchema(input) assert.ok(schema.isFluentSchema) assert.deepStrictEqual(schema.valueOf(), { ...input }) }) it('parses properties', () => { const input = S.object().prop('foo').prop('bar', S.string()).valueOf() const schema = RawSchema(input) assert.ok(schema.isFluentSchema) assert.deepStrictEqual(schema.valueOf(), { ...input }) }) it('parses nested properties', () => { const input = S.object() .prop('foo', S.object().prop('bar', S.string().minLength(3))) .valueOf() const schema = RawSchema(input) const modified = schema.prop('boom') assert.ok(modified.isFluentSchema) assert.deepStrictEqual(modified.valueOf(), { ...input, properties: { ...input.properties, boom: {} } }) }) it('parses definitions', () => { const input = S.object().definition('foo', S.string()).valueOf() const schema = RawSchema(input) assert.ok(schema.isFluentSchema) assert.deepStrictEqual(schema.valueOf(), { ...input }) }) }) describe('array', () => { it('parses type', () => { const input = S.array().items(S.string()).valueOf() const schema = RawSchema(input) assert.ok(schema.isFluentSchema) assert.deepStrictEqual(schema.valueOf(), { ...input }) }) it('parses properties', () => { const input = S.array().items(S.string()).valueOf() const schema = RawSchema(input).maxItems(1) assert.ok(schema.isFluentSchema) assert.deepStrictEqual(schema.valueOf(), { ...input, maxItems: 1 }) }) it('parses nested properties', () => { const input = S.array() .items( S.object().prop( 'foo', S.object().prop('bar', S.string().minLength(3)) ) ) .valueOf() const schema = RawSchema(input) const modified = schema.maxItems(1) assert.ok(modified.isFluentSchema) assert.deepStrictEqual(modified.valueOf(), { ...input, maxItems: 1 }) }) it('parses definitions', () => { const input = S.object().definition('foo', S.string()).valueOf() const schema = RawSchema(input) assert.ok(schema.isFluentSchema) assert.deepStrictEqual(schema.valueOf(), { ...input }) }) }) }) ================================================ FILE: src/StringSchema.js ================================================ 'use strict' const { BaseSchema } = require('./BaseSchema') const { FORMATS, setAttribute, FluentSchemaError } = require('./utils') const initialState = { type: 'string', // properties: [], //FIXME it shouldn't be set for a string because it has only attributes required: [] } /** * Represents a StringSchema. * @param {Object} [options] - Options * @param {StringSchema} [options.schema] - Default schema * @param {boolean} [options.generateIds = false] - generate the id automatically e.g. #properties.foo * @returns {StringSchema} */ // https://medium.com/javascript-scene/javascript-factory-functions-with-es6-4d224591a8b1 // Factory Functions for Mixin Composition withBaseSchema const StringSchema = ( { schema, ...options } = { schema: initialState, generateIds: false, factory: StringSchema } ) => ({ ...BaseSchema({ ...options, schema }), /* /!** * Set a property to type string * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.3|reference} * @returns {StringSchema} *!/ string: () => StringSchema({ schema: { ...schema }, ...options }).as('string'), */ /** * A string instance is valid against this keyword if its length is greater than, or equal to, the value of this keyword. * The length of a string instance is defined as the number of its characters as defined by RFC 7159 [RFC7159]. * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.3.2|reference} * @param {number} min * @returns {StringSchema} */ minLength: min => { if (!Number.isInteger(min)) { throw new FluentSchemaError("'minLength' must be an Integer") } return setAttribute({ schema, ...options }, ['minLength', min, 'string']) }, /** * A string instance is valid against this keyword if its length is less than, or equal to, the value of this keyword. * The length of a string instance is defined as the number of its characters as defined by RFC 7159 [RFC7159]. * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.3.1|reference} * @param {number} max * @returns {StringSchema} */ maxLength: max => { if (!Number.isInteger(max)) { throw new FluentSchemaError("'maxLength' must be an Integer") } return setAttribute({ schema, ...options }, ['maxLength', max, 'string']) }, /** * A string value can be RELATIVE_JSON_POINTER, JSON_POINTER, UUID, REGEX, IPV6, IPV4, HOSTNAME, EMAIL, URL, URI_TEMPLATE, URI_REFERENCE, URI, TIME, DATE, * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.7.3|reference} * @param {string} format * @returns {StringSchema} */ format: format => { if (!Object.values(FORMATS).includes(format)) { throw new FluentSchemaError( `'format' must be one of ${Object.values(FORMATS).join(', ')}` ) } return setAttribute({ schema, ...options }, ['format', format, 'string']) }, /** * This string SHOULD be a valid regular expression, according to the ECMA 262 regular expression dialect. * A string instance is considered valid if the regular expression matches the instance successfully. * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.6.3.3|reference} * @param {string} pattern * @returns {StringSchema} */ pattern: pattern => { if (!(typeof pattern === 'string') && !(pattern instanceof RegExp)) { throw new FluentSchemaError( '\'pattern\' must be a string or a RegEx (e.g. /.*/)' ) } if (pattern instanceof RegExp) { const flags = new RegExp(pattern).flags pattern = pattern .toString() .substr(1) .replace(new RegExp(`/${flags}$`), '') } return setAttribute({ schema, ...options }, ['pattern', pattern, 'string']) }, /** * If the instance value is a string, this property defines that the string SHOULD * be interpreted as binary data and decoded using the encoding named by this property. * RFC 2045, Sec 6.1 [RFC2045] lists the possible values for this property. * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.8.3|reference} * @param {string} encoding * @returns {StringSchema} */ contentEncoding: encoding => { if (!(typeof encoding === 'string')) { throw new FluentSchemaError('\'contentEncoding\' must be a string') } return setAttribute({ schema, ...options }, [ 'contentEncoding', encoding, 'string' ]) }, /** * The value of this property must be a media type, as defined by RFC 2046 [RFC2046]. * This property defines the media type of instances which this schema defines. * * {@link https://tools.ietf.org/id/draft-handrews-json-schema-validation-01.html#rfc.section.8.4|reference} * @param {string} mediaType * @returns {StringSchema} */ contentMediaType: mediaType => { if (!(typeof mediaType === 'string')) { throw new FluentSchemaError('\'contentMediaType\' must be a string') } return setAttribute({ schema, ...options }, [ 'contentMediaType', mediaType, 'string' ]) } }) module.exports = { StringSchema, FORMATS, default: StringSchema } ================================================ FILE: src/StringSchema.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const { StringSchema, FORMATS } = require('./StringSchema') const S = require('./FluentJSONSchema') describe('StringSchema', () => { it('defined', () => { assert.notStrictEqual(StringSchema, undefined) }) it('Expose symbol', () => { assert.notStrictEqual( StringSchema()[Symbol.for('fluent-schema-object')], undefined ) }) describe('constructor', () => { it('without params', () => { assert.deepStrictEqual(StringSchema().valueOf(), { type: 'string' }) }) it('from S', () => { assert.deepStrictEqual(S.string().valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', type: 'string' }) }) }) describe('keywords:', () => { describe('minLength', () => { it('valid', () => { const schema = S.object() .prop('prop', StringSchema().minLength(5)) .valueOf() assert.deepStrictEqual(schema, { $schema: 'http://json-schema.org/draft-07/schema#', properties: { prop: { type: 'string', minLength: 5 } }, type: 'object' }) }) it('invalid', () => { assert.throws( () => StringSchema().minLength('5.1'), (err) => err instanceof S.FluentSchemaError && err.message === "'minLength' must be an Integer" ) }) }) describe('maxLength', () => { it('valid', () => { const schema = StringSchema().maxLength(10).valueOf() assert.deepStrictEqual(schema, { type: 'string', maxLength: 10 }) }) it('invalid', () => { assert.throws( () => StringSchema().maxLength('5.1'), (err) => err instanceof S.FluentSchemaError && err.message === "'maxLength' must be an Integer" ) }) }) describe('format', () => { it('valid FORMATS.DATE', () => { assert.deepStrictEqual(StringSchema().format(FORMATS.DATE).valueOf(), { type: 'string', format: FORMATS.DATE }) }) it('valid FORMATS.DATE_TIME', () => { assert.deepStrictEqual( StringSchema().format(FORMATS.DATE_TIME).valueOf(), { type: 'string', format: 'date-time' } ) }) it('valid FORMATS.ISO_DATE_TIME', () => { assert.deepStrictEqual( StringSchema().format(FORMATS.ISO_DATE_TIME).valueOf(), { type: 'string', format: 'iso-date-time' } ) }) it('valid FORMATS.ISO_TIME', () => { assert.deepStrictEqual( StringSchema().format(FORMATS.ISO_TIME).valueOf(), { type: 'string', format: 'iso-time' } ) }) it('invalid', () => { assert.throws( () => StringSchema().format('invalid'), (err) => err instanceof S.FluentSchemaError && err.message === "'format' must be one of relative-json-pointer, json-pointer, uuid, regex, ipv6, ipv4, hostname, email, url, uri-template, uri-reference, uri, time, date, date-time, iso-time, iso-date-time" ) }) }) describe('pattern', () => { it('as a string', () => { assert.deepStrictEqual(StringSchema().pattern('\\/.*\\/').valueOf(), { type: 'string', pattern: '\\/.*\\/' }) }) it('as a regex without flags', () => { assert.deepStrictEqual( StringSchema() .pattern(/\/.*\//) .valueOf(), { type: 'string', pattern: '\\/.*\\/' } ) }) it('as a regex with flags', () => { assert.deepStrictEqual( StringSchema() .pattern(/\/.*\//gi) .valueOf(), { type: 'string', pattern: '\\/.*\\/' } ) }) it('invalid value', () => { assert.throws( () => StringSchema().pattern(1111), (err) => err instanceof S.FluentSchemaError && err.message === "'pattern' must be a string or a RegEx (e.g. /.*/)" ) }) }) describe('contentEncoding', () => { it('valid', () => { assert.deepStrictEqual( StringSchema().contentEncoding('base64').valueOf(), { type: 'string', contentEncoding: 'base64' } ) }) it('invalid', () => { assert.throws( () => StringSchema().contentEncoding(1000), (err) => err instanceof S.FluentSchemaError && err.message === "'contentEncoding' must be a string" ) }) }) describe('contentMediaType', () => { it('valid', () => { assert.deepStrictEqual( StringSchema().contentMediaType('image/png').valueOf(), { type: 'string', contentMediaType: 'image/png' } ) }) it('invalid', () => { assert.throws( () => StringSchema().contentMediaType(1000), (err) => err instanceof S.FluentSchemaError && err.message === "'contentMediaType' must be a string" ) }) }) describe('raw', () => { it('allows to add a custom attribute', () => { const schema = StringSchema().raw({ customKeyword: true }).valueOf() assert.deepStrictEqual(schema, { type: 'string', customKeyword: true }) }) it('allows to mix custom attibutes with regular one', () => { const schema = StringSchema() .format('date') .raw({ formatMaximum: '2020-01-01' }) .valueOf() assert.deepStrictEqual(schema, { type: 'string', formatMaximum: '2020-01-01', format: 'date' }) }) }) }) it('works', () => { const schema = S.object() .id('http://bar.com/object') .title('A object') .description('A object desc') .prop( 'name', StringSchema() .id('http://foo.com/string') .title('A string') .description('A string desc') .pattern(/.*/g) .format('date-time') ) .valueOf() assert.deepStrictEqual(schema, { $id: 'http://bar.com/object', $schema: 'http://json-schema.org/draft-07/schema#', description: 'A object desc', properties: { name: { $id: 'http://foo.com/string', description: 'A string desc', title: 'A string', type: 'string', format: 'date-time', pattern: '.*' } }, title: 'A object', type: 'object' }) }) }) ================================================ FILE: src/example.js ================================================ 'use strict' const S = require('./FluentJSONSchema') const Ajv = require('ajv') 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)) 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) /* [ { 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'", }, ] */ user = { email: 'test', password: 'password' } valid = validate(user) console.log({ valid }) //= > {valid: false} console.log(validate.errors) /* [ { keyword: 'format', dataPath: '.email', schemaPath: '#/properties/email/format', params: { format: 'email' }, message: 'should match format "email"' } ] */ user = { email: 'test@foo.com', password: 'password' } valid = validate(user) console.log({ valid }) //= > {valid: true} console.log(validate.errors) // => null user = { email: 'test@foo.com', password: 'password', address: { line1: '' } } valid = validate(user) console.log({ valid }) //= > {valid: false} console.log(validate.errors) /* { valid: false } [ { 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\'' } ] */ 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) .valueOf() console.log(userSchema.valueOf()) ================================================ FILE: src/schemas/basic.json ================================================ [ { "description": "basic schema from z-schema benchmark (https://github.com/zaggino/z-schema)", "schema": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Product set", "type": "array", "items": { "title": "Product", "type": "object", "properties": { "uuid": { "description": "The unique identifier for a product", "type": "number" }, "name": { "type": "string" }, "price": { "type": "number", "exclusiveMinimum": 0 }, "tags": { "type": "array", "items": { "type": "string" }, "minItems": 1, "uniqueItems": true }, "dimensions": { "type": "object", "properties": { "length": { "type": "number" }, "width": { "type": "number" }, "height": { "type": "number" } }, "required": ["length", "width", "height"] }, "warehouseLocation": { "description": "Coordinates of the warehouse with the product", "type": "string" } }, "required": ["uuid", "name", "price"] } }, "tests": [ { "description": "valid array from z-schema benchmark", "data": [ { "id": 2, "name": "An ice sculpture", "price": 12.5, "tags": ["cold", "ice"], "dimensions": { "length": 7.0, "width": 12.0, "height": 9.5 }, "warehouseLocation": { "latitude": -78.75, "longitude": 20.4 } }, { "id": 3, "name": "A blue mouse", "price": 25.5, "dimensions": { "length": 3.1, "width": 1.0, "height": 1.0 }, "warehouseLocation": { "latitude": 54.4, "longitude": -32.7 } } ], "valid": true }, { "description": "not array", "data": 1, "valid": false }, { "description": "array of not onjects", "data": [1, 2, 3], "valid": false }, { "description": "missing required properties", "data": [{}], "valid": false }, { "description": "required property of wrong type", "data": [{ "id": 1, "name": "product", "price": "not valid" }], "valid": false }, { "description": "smallest valid product", "data": [{ "id": 1, "name": "product", "price": 100 }], "valid": true }, { "description": "tags should be array", "data": [{ "tags": {}, "id": 1, "name": "product", "price": 100 }], "valid": false }, { "description": "dimensions should be object", "data": [ { "dimensions": [], "id": 1, "name": "product", "price": 100 } ], "valid": false }, { "description": "valid product with tag", "data": [ { "tags": ["product"], "id": 1, "name": "product", "price": 100 } ], "valid": true }, { "description": "dimensions miss required properties", "data": [ { "dimensions": {}, "tags": ["product"], "id": 1, "name": "product", "price": 100 } ], "valid": false }, { "description": "valid product with tag and dimensions", "data": [ { "dimensions": { "length": 7, "width": 12, "height": 9.5 }, "tags": ["product"], "id": 1, "name": "product", "price": 100 } ], "valid": true } ] } ] ================================================ FILE: src/utils.js ================================================ 'use strict' const deepmerge = require('@fastify/deepmerge') const isFluentSchema = (obj) => obj?.isFluentSchema const hasCombiningKeywords = (attributes) => attributes.allOf || attributes.anyOf || attributes.oneOf || attributes.not class FluentSchemaError extends Error { constructor (message) { super(message) this.name = 'FluentSchemaError' } } const last = (array) => { if (!array) return const [prop] = [...array].reverse() return prop } const isUniq = (array) => array.filter((v, i, a) => a.indexOf(v) === i).length === array.length const isBoolean = (value) => typeof value === 'boolean' const omit = (obj, props) => Object.entries(obj).reduce((memo, [key, value]) => { if (!props.includes(key)) { memo[key] = value } return memo }, {}) const flat = (array) => array.reduce((memo, prop) => { const { name, ...rest } = prop memo[name] = rest return memo }, {}) const combineArray = (options) => { const { clone, isMergeableObject, deepmerge } = options return (target, source) => { const result = target.slice() source.forEach((item, index) => { const prop = target.find((attr) => attr.name === item.name) if (result[index] === undefined) { result[index] = clone(item) } else if (isMergeableObject(prop)) { const propIndex = target.findIndex((prop) => prop.name === item.name) result[propIndex] = deepmerge(prop, item) } else if (target.indexOf(item) === -1) { result.push(item) } }) return result } } const combineDeepmerge = deepmerge({ mergeArray: combineArray }) const toArray = (obj) => obj && Object.entries(obj).map(([key, value]) => ({ name: key, ...value })) const REQUIRED = Symbol('required') const FLUENT_SCHEMA = Symbol.for('fluent-schema-object') const RELATIVE_JSON_POINTER = 'relative-json-pointer' const JSON_POINTER = 'json-pointer' const UUID = 'uuid' const REGEX = 'regex' const IPV6 = 'ipv6' const IPV4 = 'ipv4' const HOSTNAME = 'hostname' const EMAIL = 'email' const URL = 'url' const URI_TEMPLATE = 'uri-template' const URI_REFERENCE = 'uri-reference' const URI = 'uri' const TIME = 'time' const DATE = 'date' const DATE_TIME = 'date-time' const ISO_TIME = 'iso-time' const ISO_DATE_TIME = 'iso-date-time' const FORMATS = { RELATIVE_JSON_POINTER, JSON_POINTER, UUID, REGEX, IPV6, IPV4, HOSTNAME, EMAIL, URL, URI_TEMPLATE, URI_REFERENCE, URI, TIME, DATE, DATE_TIME, ISO_TIME, ISO_DATE_TIME } const STRING = 'string' const NUMBER = 'number' const BOOLEAN = 'boolean' const INTEGER = 'integer' const OBJECT = 'object' const ARRAY = 'array' const NULL = 'null' const TYPES = { STRING, NUMBER, BOOLEAN, INTEGER, OBJECT, ARRAY, NULL } const patchIdsWithParentId = ({ schema, generateIds, parentId }) => { const properties = Object.entries(schema.properties || {}) if (properties.length === 0) return schema return { ...schema, properties: properties.reduce((memo, [key, props]) => { const $id = props.$id || (generateIds ? `#properties/${key}` : undefined) memo[key] = { ...props, $id: generateIds && parentId ? `${parentId}/${$id.replace('#', '')}` : $id // e.g. #properties/foo/properties/bar } return memo }, {}) } } const appendRequired = ({ attributes: { name, required, ...attributes }, schema }) => { const { schemaRequired, attributeRequired } = (required || []).reduce( (memo, item) => { if (item === REQUIRED) { // Append prop name to the schema.required memo.schemaRequired.push(name) } else { // Propagate required attributes memo.attributeRequired.push(item) } return memo }, { schemaRequired: [], attributeRequired: [] } ) const patchedRequired = [...schema.required, ...schemaRequired] if (!isUniq(patchedRequired)) { throw new FluentSchemaError( "'required' has repeated keys, check your calls to .required()" ) } const schemaPatched = { ...schema, required: patchedRequired } const attributesPatched = { ...attributes, required: attributeRequired } return [schemaPatched, attributesPatched] } const setAttribute = ({ schema, ...options }, attribute) => { const [key, value] = attribute const currentProp = last(schema.properties) if (currentProp) { const { name, ...props } = currentProp return options.factory({ schema, ...options }).prop(name, { [key]: value, ...props }) } return options.factory({ schema: { ...schema, [key]: value }, ...options }) } const setRaw = ({ schema, ...options }, raw) => { const currentProp = last(schema.properties) if (currentProp) { const { name, ...props } = currentProp return options.factory({ schema, ...options }).prop(name, { ...raw, ...props }) } return options.factory({ schema: { ...schema, ...raw }, ...options }) } // TODO LS maybe we can just use setAttribute and remove this one const setComposeType = ({ prop, schemas, schema, options }) => { if (!(Array.isArray(schemas) && schemas.every((v) => isFluentSchema(v)))) { throw new FluentSchemaError( `'${prop}' must be a an array of FluentSchema rather than a '${typeof schemas}'` ) } const values = schemas.map((schema) => { const { $schema, ...props } = schema.valueOf({ isRoot: false }) return props }) return options.factory({ schema: { ...schema, [prop]: values }, ...options }) } module.exports = { isFluentSchema, hasCombiningKeywords, FluentSchemaError, last, isUniq, isBoolean, flat, toArray, omit, REQUIRED, patchIdsWithParentId, appendRequired, setRaw, setAttribute, setComposeType, combineDeepmerge, FORMATS, TYPES, FLUENT_SCHEMA } ================================================ FILE: src/utils.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const { setRaw, combineDeepmerge } = require('./utils') const { StringSchema } = require('./StringSchema') const { ObjectSchema } = require('./ObjectSchema') describe('setRaw', () => { it('add an attribute to a prop using ObjectSchema', () => { const factory = ObjectSchema const schema = setRaw( { schema: { properties: [{ name: 'foo', type: 'string' }] }, factory }, { nullable: true } ) assert.deepStrictEqual(schema.valueOf(), { properties: { foo: { nullable: true, type: 'string' } } }) }) it('add an attribute to a prop using StringSchema', () => { const factory = StringSchema const schema = setRaw( { schema: { type: 'string', properties: [] }, factory }, { nullable: true } ) assert.deepStrictEqual(schema.valueOf(), { nullable: true, type: 'string' }) }) }) describe('combineDeepmerge', () => { it('should merge empty arrays', () => { const result = combineDeepmerge([], []) assert.deepStrictEqual(result, []) }) it('should merge array with primitive values', () => { const result = combineDeepmerge([1], [2]) assert.deepStrictEqual(result, [1, 2]) }) it('should merge arrays with primitive values', () => { const result = combineDeepmerge([], [1, 2]) assert.deepStrictEqual(result, [1, 2]) }) it('should merge arrays with primitive values', () => { const result = combineDeepmerge([1, 2], [1, 2, 3]) assert.deepStrictEqual(result, [1, 2, 3]) }) it('should merge array with simple Schemas', () => { const result = combineDeepmerge([{ type: 'string' }], [{ type: 'string' }]) assert.deepStrictEqual(result, [{ type: 'string' }]) }) it('should merge array with named Schemas', () => { const result = combineDeepmerge( [{ name: 'variant 1', type: 'string' }], [{ name: 'variant 2', type: 'string' }] ) assert.deepStrictEqual(result, [ { name: 'variant 1', type: 'string' }, { name: 'variant 2', type: 'string' } ]) }) it('should merge array with same named Schemas', () => { const result = combineDeepmerge( [{ name: 'variant 2', type: 'string' }], [{ name: 'variant 2', type: 'number' }] ) assert.deepStrictEqual(result, [{ name: 'variant 2', type: 'number' }]) }) it('should merge array with same named Schemas', () => { const result = combineDeepmerge( [{ name: 'variant 2', type: 'string' }], [ { name: 'variant 2', type: 'number' }, { name: 'variant 1', type: 'string' } ] ) assert.deepStrictEqual(result, [ { name: 'variant 2', type: 'number' }, { name: 'variant 1', type: 'string' } ]) }) }) ================================================ FILE: types/FluentJSONSchema.d.ts ================================================ export interface BaseSchema { id: (id: string) => T title: (title: string) => T description: (description: string) => T examples: (examples: Array) => T ref: (ref: string) => T enum: (values: Array) => T const: (value: any) => T default: (value: any) => T required: (fields?: string[]) => T ifThen: (ifClause: JSONSchema, thenClause: JSONSchema) => T ifThenElse: ( ifClause: JSONSchema, thenClause: JSONSchema, elseClause: JSONSchema ) => T not: (schema: JSONSchema) => T anyOf: (schema: Array) => T allOf: (schema: Array) => T oneOf: (schema: Array) => T readOnly: (isReadOnly?: boolean) => T writeOnly: (isWriteOnly?: boolean) => T deprecated: (isDeprecated?: boolean) => T isFluentSchema: boolean isFluentJSONSchema: boolean raw: (fragment: any) => T } export type TYPE = | 'string' | 'number' | 'boolean' | 'integer' | 'object' | 'array' | 'null' type FORMATS = { RELATIVE_JSON_POINTER: 'relative-json-pointer' JSON_POINTER: 'json-pointer' UUID: 'uuid' REGEX: 'regex' IPV6: 'ipv6' IPV4: 'ipv4' HOSTNAME: 'hostname' EMAIL: 'email' URL: 'url' URI_TEMPLATE: 'uri-template' URI_REFERENCE: 'uri-reference' URI: 'uri' TIME: 'time' DATE: 'date' DATE_TIME: 'date-time' ISO_TIME: 'iso-time' ISO_DATE_TIME: 'iso-date-time' } export type JSONSchema = | ObjectSchema | StringSchema | NumberSchema | ArraySchema | IntegerSchema | BooleanSchema | NullSchema | ExtendedSchema export class FluentSchemaError extends Error { name: string } export interface SchemaOptions { schema: object generateIds: boolean } export interface StringSchema extends BaseSchema { minLength: (min: number) => StringSchema maxLength: (min: number) => StringSchema format: (format: FORMATS[keyof FORMATS]) => StringSchema pattern: (pattern: string | RegExp) => StringSchema contentEncoding: (encoding: string) => StringSchema contentMediaType: (mediaType: string) => StringSchema } export interface NullSchema { null: () => StringSchema } export interface BooleanSchema extends BaseSchema { boolean: () => BooleanSchema } export interface NumberSchema extends BaseSchema { minimum: (min: number) => NumberSchema exclusiveMinimum: (min: number) => NumberSchema maximum: (max: number) => NumberSchema exclusiveMaximum: (max: number) => NumberSchema multipleOf: (multiple: number) => NumberSchema } export interface IntegerSchema extends BaseSchema { minimum: (min: number) => IntegerSchema exclusiveMinimum: (min: number) => IntegerSchema maximum: (max: number) => IntegerSchema exclusiveMaximum: (max: number) => IntegerSchema multipleOf: (multiple: number) => IntegerSchema } export interface ArraySchema extends BaseSchema { items: (items: JSONSchema | Array) => ArraySchema additionalItems: (items: Array | boolean) => ArraySchema contains: (value: JSONSchema | boolean) => ArraySchema uniqueItems: (boolean: boolean) => ArraySchema minItems: (min: number) => ArraySchema maxItems: (max: number) => ArraySchema } export interface ObjectSchema = Record> extends BaseSchema> { definition: (name: Key, props?: JSONSchema) => ObjectSchema prop: (name: Key, props?: JSONSchema) => ObjectSchema additionalProperties: (value: JSONSchema | boolean) => ObjectSchema maxProperties: (max: number) => ObjectSchema minProperties: (min: number) => ObjectSchema patternProperties: (options: PatternPropertiesOptions) => ObjectSchema dependencies: (options: DependenciesOptions) => ObjectSchema propertyNames: (value: JSONSchema) => ObjectSchema extend: (schema: ObjectSchema | ExtendedSchema) => ExtendedSchema only: (properties: string[]) => ObjectSchema without: (properties: string[]) => ObjectSchema dependentRequired: (options: DependentRequiredOptions) => ObjectSchema dependentSchemas: (options: DependentSchemaOptions) => ObjectSchema } export type ExtendedSchema = Pick type InferSchemaMap = { string: StringSchema number: NumberSchema boolean: BooleanSchema integer: IntegerSchema object: ObjectSchema array: ArraySchema null: NullSchema } export type MixedSchema = T extends readonly [infer First extends TYPE, ...infer Rest extends TYPE[]] ? InferSchemaMap[First] & MixedSchema : unknown interface PatternPropertiesOptions { [key: string]: JSONSchema } interface DependenciesOptions { [key: string]: JSONSchema[] } type Key = keyof T | (string & {}) type DependentSchemaOptions>> = Partial> type DependentRequiredOptions>> = Partial> export function withOptions (options: SchemaOptions): T type ObjectPlaceholder = Record export interface S extends BaseSchema { string: () => StringSchema number: () => NumberSchema integer: () => IntegerSchema boolean: () => BooleanSchema array: () => ArraySchema object: () => ObjectSchema null: () => NullSchema mixed(types: T): MixedSchema raw: (fragment: any) => S FORMATS: FORMATS } // eslint-disable-next-line @typescript-eslint/no-redeclare, no-var export declare var S: S export default S ================================================ FILE: types/FluentJSONSchema.test-d.ts ================================================ // This file will be passed to the TypeScript CLI to verify our typings compile import S, { FluentSchemaError } from '..' console.log('isFluentSchema:', S.object().isFluentJSONSchema) const schema = S.object() .id('http://foo.com/user') .title('A User') .description('A User desc') .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') .allOf([S.string()]) .prop('city') .prop('zipcode') ) .prop('username', S.string().pattern(/[a-z]*/g)) .prop('email', S.string().format('email')) .prop('email2', S.string().format(S.FORMATS.EMAIL)) .prop( 'avatar', S.string().contentEncoding('base64').contentMediaType('image/png') ) .required() .prop( 'password', S.string().default('123456').minLength(6).maxLength(12).pattern('.*') ) .required() .prop('addresses', S.array().items([S.ref('#address')])) .required() .prop( 'role', S.object() .id('http://foo.com/role') .prop('name') .enum(['ADMIN', 'USER']) .prop('permissions') ) .required() .prop('age', S.mixed(['string', 'integer'])) .ifThen(S.object().prop('age', S.string()), S.required(['age'])) .readOnly() .writeOnly(true) .valueOf() console.log('example:\n', JSON.stringify(schema)) console.log('isFluentSchema:', S.object().isFluentSchema) 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('user:\n', JSON.stringify(userSchema.valueOf())) const largeUserSchema = 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 userSubsetSchema = largeUserSchema.only(['username', 'password']) console.log('user subset:', JSON.stringify(userSubsetSchema.valueOf())) 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']) console.log('person subset:', JSON.stringify(bodySchema.valueOf())) const personSchemaAllowsUnix = S.object() .prop('name', S.string()) .prop('age', S.number()) .prop('id', S.string().format('uuid')) .prop('createdAt', S.mixed(['string', 'integer']).format('time')) .prop('updatedAt', S.mixed(['string', 'integer']).minimum(0)) console.log('person schema allows unix:', JSON.stringify(personSchemaAllowsUnix.valueOf())) try { S.object().prop('foo', 'boom!' as any) } catch (e) { if (e instanceof FluentSchemaError) { console.log(e.message) } } const arrayExtendedSchema = S.array().items(userSchema).valueOf() console.log('array of user\n', JSON.stringify(arrayExtendedSchema)) const extendExtendedSchema = S.object().extend(userSchema) console.log('extend of user\n', JSON.stringify(extendExtendedSchema)) const rawNullableSchema = S.object() .raw({ nullable: true }) .required(['foo', 'hello']) .prop('foo', S.string()) .prop('hello', S.string()) console.log('raw schema with nullable props\n', JSON.stringify(rawNullableSchema)) const dependentRequired = S.object() .dependentRequired({ foo: ['bar'], }) .prop('foo') .prop('bar') .valueOf() console.log('dependentRequired:\n', JSON.stringify(dependentRequired)) const dependentSchemas = S.object() .dependentSchemas({ foo: S.object().prop('bar'), }) .prop('bar', S.object().prop('bar')) .valueOf() console.log('dependentRequired:\n', JSON.stringify(dependentSchemas)) const deprecatedSchema = S.object() .deprecated() .prop('foo', S.string().deprecated()) .valueOf() console.log('deprecatedSchema:\n', JSON.stringify(deprecatedSchema)) type Foo = { foo: string bar: string } const dependentRequiredWithType = S.object() .dependentRequired({ foo: ['bar'], }) .prop('foo') .prop('bar') .valueOf() console.log('dependentRequired:\n', JSON.stringify(dependentRequiredWithType)) const dependentSchemasWithType = S.object() .dependentSchemas({ foo: S.object().prop('bar'), }) .prop('bar', S.object().prop('bar')) .valueOf() console.log('dependentSchemasWithType:\n', JSON.stringify(dependentSchemasWithType)) const deprecatedSchemaWithType = S.object() .deprecated() .prop('foo', S.string().deprecated()) .valueOf() console.log('deprecatedSchemaWithType:\n', JSON.stringify(deprecatedSchemaWithType)) type ReallyLongType = { foo: string bar: string baz: string xpto: string abcd: number kct: { a: string b: number d: null } } const deepTestOnTypes = S.object() .prop('bar', S.object().prop('bar')) // you can provide any string, to avoid breaking changes .prop('aaaa', S.anyOf([S.string()])) .definition('abcd', S.number()) .valueOf() console.log('deepTestOnTypes:\n', JSON.stringify(deepTestOnTypes)) const tsIsoSchema = S.object() .prop('createdAt', S.string().format('iso-time')) .prop('updatedAt', S.string().format('iso-date-time')) .valueOf() console.log('ISO schema OK:', JSON.stringify(tsIsoSchema))