diff --git a/README.md b/README.md index aa3baaab5..c10422e63 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Supports JSON Schema draft-04/06/07/2019-09/2020-12 ([draft-04 support](https:// [![npm](https://img.shields.io/npm/v/ajv.svg)](https://www.npmjs.com/package/ajv) [![npm downloads](https://img.shields.io/npm/dm/ajv.svg)](https://www.npmjs.com/package/ajv) [![Coverage Status](https://coveralls.io/repos/github/ajv-validator/ajv/badge.svg?branch=master)](https://coveralls.io/github/ajv-validator/ajv?branch=master) -[![SimpleX](https://img.shields.io/badge/chat-on%20SimpleX-%2307b4b9)](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2Fap4lMFzfXF8Hzmh-Vz0WNxp_1jKiOa-h%23MCowBQYDK2VuAyEAcdefddRvDfI8iAuBpztm_J3qFucj8MDZoVs_2EcMTzU%3D) +[![SimpleX](https://img.shields.io/badge/chat-on%20SimpleX-70F0F9)](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F8KvvURM6J38Gdq9dCuPswMOkMny0xCOJ%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAr8rPVRuMOXv6kwF2yUAap-eoVg-9ssOFCi1fIrxTUw0%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%224pwLRgWHU9tlroMWHz0uOg%3D%3D%22%7D) [![Gitter](https://img.shields.io/gitter/room/ajv-validator/ajv.svg)](https://gitter.im/ajv-validator/ajv) [![GitHub Sponsors](https://img.shields.io/badge/$-sponsors-brightgreen)](https://github.com/sponsors/epoberezkin) @@ -35,6 +35,7 @@ Please review [Contributing guidelines](./CONTRIBUTING.md) and [Code components] All documentation is available on the [Ajv website](https://ajv.js.org). Some useful site links: + - [Getting started](https://ajv.js.org/guide/getting-started.html) - [JSON Schema vs JSON Type Definition](https://ajv.js.org/guide/schema-language.html) - [API reference](https://ajv.js.org/api.html) @@ -53,7 +54,7 @@ Your continuing support is very important - the funds will be used to develop an Please sponsor Ajv via: - [GitHub sponsors page](https://github.com/sponsors/epoberezkin) (GitHub will match it) -- [Ajv Open Collective️](https://opencollective.com/ajv) +- [Ajv Open Collective](https://opencollective.com/ajv) Thank you. @@ -73,6 +74,19 @@ Thank you. + + + + + + + + + + + + + ## Performance @@ -87,7 +101,7 @@ Currently Ajv is the fastest and the most standard compliant validator according Performance of different validators by [json-schema-benchmark](https://github.com/ebdrup/json-schema-benchmark): -[![performance](https://chart.googleapis.com/chart?chxt=x,y&cht=bhs&chco=76A4FB&chls=2.0&chbh=62,4,1&chs=600x416&chxl=-1:|ajv|@exodus/schemasafe|is-my-json-valid|djv|@cfworker/json-schema|jsonschema&chd=t:100,69.2,51.5,13.1,5.1,1.2)](https://github.com/ebdrup/json-schema-benchmark/blob/master/README.md#performance) +[![performance](https://chart.googleapis.com/chart?chxt=x,y&cht=bhs&chco=76A4FB&chls=2.0&chbh=62,4,1&chs=600x416&chxl=-1:|ajv|@exodus/schemasafe|is-my-json-valid|djv|@cfworker/json-schema|jsonschema/=t:100,69.2,51.5,13.1,5.1,1.2)](https://github.com/ebdrup/json-schema-benchmark/blob/master/README.md#performance) ## Features @@ -144,7 +158,7 @@ const schema = { type: "object", properties: { foo: {type: "integer"}, - bar: {type: "string"} + bar: {type: "string"}, }, required: ["foo"], additionalProperties: false, @@ -152,7 +166,7 @@ const schema = { const data = { foo: 1, - bar: "abc" + bar: "abc", } const validate = ajv.compile(schema) diff --git a/docs/api.md b/docs/api.md index 33ea7af9a..5dfb0c309 100644 --- a/docs/api.md +++ b/docs/api.md @@ -149,7 +149,7 @@ Every time this method is called the errors are overwritten so you need to copy If the schema is asynchronous (has `$async` keyword on the top level) this method returns a Promise. See [Asynchronous validation](./guide/async-validation.md). - + ### ajv.addSchema(schema: object | object[], key?: string): Ajv @@ -245,7 +245,7 @@ Formats can be also added via `formats` option. -### ajv.addKeyword(definition: object): Ajv +### ajv.addKeyword(definition: string | object): Ajv Add validation keyword to Ajv instance. @@ -297,6 +297,8 @@ interface KeywordDefinition { } ``` +If only the property `keyword` is provided in the definition object, you can also pass the keyword name as the argument. + `compile`, `macro` and `code` are mutually exclusive, only one should be used at a time. `validate` can be used separately or in addition to `compile` or `macro` to support [\$data reference](./guide/combining-schemas.md#data-reference). ::: tip Keyword is validated only for applicable data types diff --git a/docs/guide/combining-schemas.md b/docs/guide/combining-schemas.md index 7937fe150..410c7fb6e 100644 --- a/docs/guide/combining-schemas.md +++ b/docs/guide/combining-schemas.md @@ -41,7 +41,7 @@ const ajv = new Ajv() const validate = ajv.addSchema(defsSchema).compile(schema) ``` -See [Options](./api.md#options) and [addSchema](./api.md#add-schema) method. +See [Options](../options.md) and [addSchema](../api.md#add-schema) method. ::: tip Reference resolution - `$ref` is resolved as the uri-reference using schema \$id as the base URI (see the example). diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index e29f5a4e7..a6fbff244 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -8,7 +8,7 @@ You can try Ajv without installing it in the Node.js REPL: [https://runkit.com/npm/ajv](https://runkit.com/npm/ajv) ::: -To install Ajv version 7: +To install Ajv version 8: ```bash npm install ajv @@ -140,8 +140,8 @@ const parse = ajv.compileParser(schema) const json = '{"foo": 1, "bar": "abc"}' const invalidJson = '{"unknown": "abc"}' -console.log(parseAndLog(json)) // logs {foo: 1, bar: "abc"} -console.log(parseAndLog(invalidJson)) // logs error and position +parseAndLog(json) // logs {foo: 1, bar: "abc"} +parseAndLog(invalidJson) // logs error and position function parseAndLog(json) { const data = parse(json) diff --git a/docs/guide/modifying-data.md b/docs/guide/modifying-data.md index 5c1991916..2705c0bed 100644 --- a/docs/guide/modifying-data.md +++ b/docs/guide/modifying-data.md @@ -154,7 +154,7 @@ See [discriminator](../json-schema.md#discriminator) keyword. ## Assigning defaults -With [option `useDefaults`](./api.md#options) Ajv will assign values from `default` keyword in the schemas of `properties` and `items` (when it is the array of schemas) to the missing properties and items. +With [option `useDefaults`](./options.md#options) Ajv will assign values from `default` keyword in the schemas of `properties` and `items` (when it is the array of schemas) to the missing properties and items. With the option value `"empty"` properties and items equal to `null` or `""` (empty string) will be considered missing and assigned defaults. diff --git a/docs/guide/schema-language.md b/docs/guide/schema-language.md index 4b87903a3..a10fc8f98 100644 --- a/docs/guide/schema-language.md +++ b/docs/guide/schema-language.md @@ -2,6 +2,7 @@ tags: - JTD --- + # Choosing schema language [[toc]] @@ -123,17 +124,19 @@ See [JSON Schema](../json-schema.md) for more information and the list of define - Defines the shape of JSON data via strictly defined schema forms (rather than the collection of restrictions). - Effective support for tagged unions. - Designed to protect against user mistakes. -- Supports compilation of schemas to efficient [serializers and parsers](./getting-started.md#parsing-and-serializing-json) (no need to validate as a separate step) -- Approved as [RFC8927](https://datatracker.ietf.org/doc/rfc8927/) +- Supports compilation of schemas to efficient [serializers and parsers](./getting-started.md#parsing-and-serializing-json) (no need to validate as a separate step). +- Approved as [RFC8927](https://datatracker.ietf.org/doc/rfc8927/). +- Substantial industry adoption since it was standardized in 2020, Ajv v8.12.0 fixed all reported JTD bugs. **Cons**: -- Limited, compared with JSON Schema - no support for untagged unions\*, conditionals, references between different schema files\*\*, etc. -- No meta-schema in the specification\*. -- Brand new - limited industry adoption (as of January 2021). +- Limited, compared with JSON Schema - no support for untagged unions1, conditionals, references between different schema files2, etc. +- No meta-schema in the specification3. + +1 Ajv defines non-standard keyword "union" that can be used inside "metadata" object. -\* Ajv defines meta-schema for JTD schemas and non-standard keyword "union" that can be used inside "metadata" object. +2 You can still combine schemas from multiple files in the application code. -\*\* You can still combine schemas from multiple files in the application code. +3 Ajv defines meta-schema for JTD schemas. See [JSON Type Definition](../json-type-definition.md) for more information and the list of defined schema forms. diff --git a/docs/json-type-definition.md b/docs/json-type-definition.md index 41f13832d..d12326b34 100644 --- a/docs/json-type-definition.md +++ b/docs/json-type-definition.md @@ -416,6 +416,10 @@ Any user-defined keywords that can be used in JSON Schema schemas can also be us It is strongly recommended to only use it to simplify migration from JSON Schema to JTD and not to use non-standard keywords in the new schemas, as these keywords are not supported by any other tools. ::: +::: warning Parsing does NOT support non-standard JTD keywords +compileParser method does not support non-standard JTD keywords, you will have to use JSON.parse and then validates. +::: + ## Validation errors TODO diff --git a/docs/keywords.md b/docs/keywords.md index 3f61a68c2..752fa61eb 100644 --- a/docs/keywords.md +++ b/docs/keywords.md @@ -239,7 +239,7 @@ ajv.addKeyword({ }) ``` -Macro keywords an be recursive - i.e. return schemas containing the same keyword. See the example of defining a recursive macro keyword `deepProperties` in the [test](https://github.com/ajv-validator/ajv/blob/master/spec/keyword.spec.ts#L316). +Macro keywords can be recursive - i.e. return schemas containing the same keyword. See the example of defining a recursive macro keyword `deepProperties` in the [test](https://github.com/ajv-validator/ajv/blob/master/spec/keyword.spec.ts#L316). ## Schema compilation context diff --git a/docs/security.md b/docs/security.md index f4818b5b0..7ecf8b031 100644 --- a/docs/security.md +++ b/docs/security.md @@ -72,7 +72,7 @@ To use a third-party regex engine in Ajv, set the ajv.opts.code.regExp property ``` const Ajv = require("ajv") const RE2 = require("re2") -const ajv = new Ajv({regExp: RE2}) +const ajv = new Ajv({code: {regExp: RE2}}) ``` For details about the interface of the `regexp` option, see options.md under the docs folder. diff --git a/lib/2019.ts b/lib/2019.ts index 0311c4d09..45a3fa535 100644 --- a/lib/2019.ts +++ b/lib/2019.ts @@ -76,3 +76,5 @@ export {DefinedError} from "./vocabularies/errors" export {JSONType} from "./compile/rules" export {JSONSchemaType} from "./types/json-schema" export {_, str, stringify, nil, Name, Code, CodeGen, CodeGenOptions} from "./compile/codegen" +export {default as ValidationError} from "./runtime/validation_error" +export {default as MissingRefError} from "./compile/ref_error" diff --git a/lib/2020.ts b/lib/2020.ts index beff5c7bd..afbdda200 100644 --- a/lib/2020.ts +++ b/lib/2020.ts @@ -70,3 +70,5 @@ export {DefinedError} from "./vocabularies/errors" export {JSONType} from "./compile/rules" export {JSONSchemaType} from "./types/json-schema" export {_, str, stringify, nil, Name, Code, CodeGen, CodeGenOptions} from "./compile/codegen" +export {default as ValidationError} from "./runtime/validation_error" +export {default as MissingRefError} from "./compile/ref_error" diff --git a/lib/ajv.ts b/lib/ajv.ts index bbfa5a4e5..7f87c8aea 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -65,3 +65,5 @@ export {DefinedError} from "./vocabularies/errors" export {JSONType} from "./compile/rules" export {JSONSchemaType} from "./types/json-schema" export {_, str, stringify, nil, Name, Code, CodeGen, CodeGenOptions} from "./compile/codegen" +export {default as ValidationError} from "./runtime/validation_error" +export {default as MissingRefError} from "./compile/ref_error" diff --git a/lib/compile/jtd/serialize.ts b/lib/compile/jtd/serialize.ts index deb821b96..7a7385d69 100644 --- a/lib/compile/jtd/serialize.ts +++ b/lib/compile/jtd/serialize.ts @@ -119,7 +119,7 @@ function serializeValues(cxt: SerializeCxt): void { gen.add(N.json, str`}`) } -function serializeKeyValue(cxt: SerializeCxt, key: Name, schema: SchemaObject, first: Name): void { +function serializeKeyValue(cxt: SerializeCxt, key: Name, schema: SchemaObject, first?: Name): void { const {gen, data} = cxt addComma(cxt, first) serializeString({...cxt, data: key}) @@ -158,20 +158,24 @@ function serializeSchemaProperties(cxt: SerializeCxt, discriminator?: string): v const optProps = keys(optionalProperties) const allProps = allProperties(props.concat(optProps)) let first = !discriminator + let firstProp: Name | undefined + for (const key of props) { + if (first) first = false + else gen.add(N.json, str`,`) serializeProperty(key, properties[key], keyValue(key)) } + if (first) firstProp = gen.let("first", true) for (const key of optProps) { const value = keyValue(key) - gen.if(and(_`${value} !== undefined`, isOwnProperty(gen, data, key)), () => + gen.if(and(_`${value} !== undefined`, isOwnProperty(gen, data, key)), () => { + addComma(cxt, firstProp) serializeProperty(key, optionalProperties[key], value) - ) + }) } if (schema.additionalProperties) { gen.forIn("key", data, (key) => - gen.if(isAdditional(key, allProps), () => - serializeKeyValue(cxt, key, {}, gen.let("first", first)) - ) + gen.if(isAdditional(key, allProps), () => serializeKeyValue(cxt, key, {}, firstProp)) ) } @@ -192,8 +196,6 @@ function serializeSchemaProperties(cxt: SerializeCxt, discriminator?: string): v } function serializeProperty(key: string, propSchema: SchemaObject, value: Name): void { - if (first) first = false - else gen.add(N.json, str`,`) gen.add(N.json, str`${JSON.stringify(key)}:`) serializeCode({...cxt, schema: propSchema, data: value}) } @@ -253,10 +255,14 @@ function serializeEmpty({gen, data}: SerializeCxt): void { gen.add(N.json, _`JSON.stringify(${data})`) } -function addComma({gen}: SerializeCxt, first: Name): void { - gen.if( - first, - () => gen.assign(first, false), - () => gen.add(N.json, str`,`) - ) +function addComma({gen}: SerializeCxt, first?: Name): void { + if (first) { + gen.if( + first, + () => gen.assign(first, false), + () => gen.add(N.json, str`,`) + ) + } else { + gen.add(N.json, str`,`) + } } diff --git a/lib/compile/jtd/types.ts b/lib/compile/jtd/types.ts index 7f3619576..1258050fd 100644 --- a/lib/compile/jtd/types.ts +++ b/lib/compile/jtd/types.ts @@ -13,4 +13,4 @@ export const jtdForms = [ "ref", ] as const -export type JTDForm = typeof jtdForms[number] +export type JTDForm = (typeof jtdForms)[number] diff --git a/lib/compile/rules.ts b/lib/compile/rules.ts index ea65074f9..7dbf7ab9e 100644 --- a/lib/compile/rules.ts +++ b/lib/compile/rules.ts @@ -2,7 +2,7 @@ import type {AddedKeywordDefinition} from "../types" const _jsonTypes = ["string", "number", "integer", "boolean", "null", "object", "array"] as const -export type JSONType = typeof _jsonTypes[number] +export type JSONType = (typeof _jsonTypes)[number] const jsonTypes: Set = new Set(_jsonTypes) diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index 102692306..42d50324c 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -297,7 +297,7 @@ function checkContextTypes(it: SchemaObjCxt, types: JSONType[]): void { strictTypesError(it, `type "${t}" not allowed by context "${it.dataTypes.join(",")}"`) } }) - it.dataTypes = it.dataTypes.filter((t) => includesType(types, t)) + narrowSchemaTypes(it, types) } function checkMultipleTypes(it: SchemaObjCxt, ts: JSONType[]): void { @@ -327,6 +327,15 @@ function includesType(ts: JSONType[], t: JSONType): boolean { return ts.includes(t) || (t === "integer" && ts.includes("number")) } +function narrowSchemaTypes(it: SchemaObjCxt, withTypes: JSONType[]): void { + const ts: JSONType[] = [] + for (const t of it.dataTypes) { + if (includesType(withTypes, t)) ts.push(t) + else if (withTypes.includes("integer") && t === "number") ts.push("integer") + } + it.dataTypes = ts +} + function strictTypesError(it: SchemaObjCxt, msg: string): void { const schemaPath = it.schemaEnv.baseId + it.errSchemaPath msg += ` at "${schemaPath}" (strictTypes)` diff --git a/lib/jtd.ts b/lib/jtd.ts index b53749ba4..96eb7b9dc 100644 --- a/lib/jtd.ts +++ b/lib/jtd.ts @@ -127,3 +127,5 @@ export {_, str, stringify, nil, Name, Code, CodeGen, CodeGenOptions} from "./com export {JTDSchemaType, SomeJTDSchemaType, JTDDataType} export {JTDOptions} +export {default as ValidationError} from "./runtime/validation_error" +export {default as MissingRefError} from "./compile/ref_error" diff --git a/lib/types/json-schema.ts b/lib/types/json-schema.ts index 698e29116..281a38bdb 100644 --- a/lib/types/json-schema.ts +++ b/lib/types/json-schema.ts @@ -180,6 +180,7 @@ type Nullable = undefined extends T default?: T | null } : { + nullable?: false const?: T enum?: Readonly default?: T diff --git a/lib/types/jtd-schema.ts b/lib/types/jtd-schema.ts index 3d5ae4ac9..61b2bde81 100644 --- a/lib/types/jtd-schema.ts +++ b/lib/types/jtd-schema.ts @@ -74,7 +74,7 @@ type EnumString = [T] extends [never] : null /** true if type is a union of string literals */ -type IsEnum = null extends EnumString> ? false : true +type IsEnum = null extends EnumString ? false : true /** true only if all types are array types (not tuples) */ // NOTE relies on the fact that tuples don't have an index at 0.5, but arrays @@ -88,13 +88,18 @@ type IsElements = false extends IsUnion : false /** true if the the type is a values type */ -type IsValues = false extends IsUnion> - ? TypeEquality, string> +type IsValues = false extends IsUnion ? TypeEquality : false + +/** true if type is a properties type and Union is false, or type is a discriminator type and Union is true */ +type IsRecord = Union extends IsUnion + ? null extends EnumString + ? false + : true : false -/** true if type is a proeprties type and Union is false, or type is a discriminator type and Union is true */ -type IsRecord = Union extends IsUnion> - ? null extends EnumString> +/** true if type represents an empty record */ +type IsEmptyRecord = [T] extends [Record] + ? [T] extends [never] ? false : true : false @@ -131,7 +136,7 @@ export type JTDSchemaType = Record + true extends IsEnum> ? {enum: EnumString>[]} : // arrays - only accepts arrays, could be array of unions to be resolved later true extends IsElements> @@ -140,15 +145,20 @@ export type JTDSchemaType = Record } : never + : // empty properties + true extends IsEmptyRecord> + ? + | {properties: Record; optionalProperties?: Record} + | {optionalProperties: Record} : // values - true extends IsValues + true extends IsValues> ? T extends Record ? { values: JTDSchemaType } : never : // properties - true extends IsRecord + true extends IsRecord, false> ? ([RequiredKeys>] extends [never] ? { properties?: Record @@ -168,7 +178,7 @@ export type JTDSchemaType = Record + true extends IsRecord, true> ? { [K in keyof Exclude]-?: Exclude[K] extends string ? { @@ -176,7 +186,7 @@ export type JTDSchemaType = Record[K]]: JTDSchemaType< - Omit, + Omit ? T : never, K>, D > } diff --git a/lib/vocabularies/jtd/properties.ts b/lib/vocabularies/jtd/properties.ts index 728c0b92c..9dd24c5cd 100644 --- a/lib/vocabularies/jtd/properties.ts +++ b/lib/vocabularies/jtd/properties.ts @@ -138,9 +138,7 @@ export function validateProperties(cxt: KeywordCxt): void { function validateAdditional(): void { gen.forIn("key", data, (key: Name) => { - const _allProps = - it.jtdDiscriminator === undefined ? allProps : [it.jtdDiscriminator].concat(allProps) - const addProp = isAdditional(key, _allProps, "properties") + const addProp = isAdditional(key, allProps, "properties", it.jtdDiscriminator) const addOptProp = isAdditional(key, allOptProps, "optionalProperties") const extra = addProp === true ? addOptProp : addOptProp === true ? addProp : and(addProp, addOptProp) @@ -159,14 +157,23 @@ export function validateProperties(cxt: KeywordCxt): void { }) } - function isAdditional(key: Name, props: string[], keyword: string): Code | true { + function isAdditional( + key: Name, + props: string[], + keyword: string, + jtdDiscriminator?: string + ): Code | true { let additional: Code | boolean if (props.length > 8) { // TODO maybe an option instead of hard-coded 8? const propsSchema = schemaRefOrVal(it, parentSchema[keyword], keyword) additional = not(isOwnProperty(gen, propsSchema as Code, key)) - } else if (props.length) { - additional = and(...props.map((p) => _`${key} !== ${p}`)) + if (jtdDiscriminator !== undefined) { + additional = and(additional, _`${key} !== ${jtdDiscriminator}`) + } + } else if (props.length || jtdDiscriminator !== undefined) { + const ps = jtdDiscriminator === undefined ? props : [jtdDiscriminator].concat(props) + additional = and(...ps.map((p) => _`${key} !== ${p}`)) } else { additional = true } diff --git a/lib/vocabularies/jtd/values.ts b/lib/vocabularies/jtd/values.ts index 86091b8ce..e64945077 100644 --- a/lib/vocabularies/jtd/values.ts +++ b/lib/vocabularies/jtd/values.ts @@ -1,7 +1,7 @@ import type {CodeKeywordDefinition, SchemaObject} from "../../types" import type {KeywordCxt} from "../../compile/validate" import {alwaysValidSchema, Type} from "../../compile/util" -import {not, Name} from "../../compile/codegen" +import {not, or, Name} from "../../compile/codegen" import {checkMetadata} from "./metadata" import {checkNullableObject} from "./nullable" import {typeError, _JTDTypeError} from "./error" @@ -15,13 +15,16 @@ const def: CodeKeywordDefinition = { code(cxt: KeywordCxt) { checkMetadata(cxt) const {gen, data, schema, it} = cxt - if (alwaysValidSchema(it, schema)) return const [valid, cond] = checkNullableObject(cxt, data) - gen.if(cond) - gen.assign(valid, validateMap()) - gen.elseIf(not(valid)) - cxt.error() - gen.endIf() + if (alwaysValidSchema(it, schema)) { + gen.if(not(or(cond, valid)), () => cxt.error()) + } else { + gen.if(cond) + gen.assign(valid, validateMap()) + gen.elseIf(not(valid)) + cxt.error() + gen.endIf() + } cxt.ok(valid) function validateMap(): Name | boolean { diff --git a/package.json b/package.json index bc0928015..71e1cff97 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@redocly/ajv", - "version": "8.11.0", + "version": "8.12.0", "description": "Another JSON Schema Validator", "main": "dist/ajv.js", "types": "dist/ajv.d.ts", @@ -67,13 +67,13 @@ }, "devDependencies": { "@ajv-validator/config": "^0.3.0", - "@rollup/plugin-commonjs": "^21.0.0", - "@rollup/plugin-json": "^4.1.0", - "@rollup/plugin-node-resolve": "^13.0.0", - "@rollup/plugin-typescript": "^8.2.1", + "@rollup/plugin-commonjs": "^24.0.0", + "@rollup/plugin-json": "^6.0.0", + "@rollup/plugin-node-resolve": "^15.0.1", + "@rollup/plugin-typescript": "^10.0.1", "@types/chai": "^4.2.12", - "@types/mocha": "^9.0.0", - "@types/node": "^17.0.0", + "@types/mocha": "^10.0.0", + "@types/node": "^18.11.9", "@types/require-from-string": "^1.2.0", "@typescript-eslint/eslint-plugin": "^3.8.0", "@typescript-eslint/parser": "^3.8.0", @@ -85,9 +85,9 @@ "dayjs-plugin-utc": "^0.1.2", "eslint": "^7.8.1", "eslint-config-prettier": "^7.0.0", - "fast-uri": "^1.0.0", + "fast-uri": "^2.1.0", "glob": "^8.0.2", - "husky": "^7.0.1", + "husky": "^8.0.2", "if-node-version": "^1.0.0", "jimp": "^0.16.1", "js-beautify": "^1.7.3", @@ -95,8 +95,8 @@ "karma": "^6.0.0", "karma-chrome-launcher": "^3.0.0", "karma-mocha": "^2.0.0", - "lint-staged": "^12.1.1", - "mocha": "^9.0.2", + "lint-staged": "^13.0.3", + "mocha": "^10.0.0", "module-from-string": "^3.1.3", "node-fetch": "^3.0.0", "nyc": "^15.0.0", @@ -106,7 +106,7 @@ "rollup-plugin-terser": "^7.0.2", "ts-node": "^10.0.0", "tsify": "^5.0.2", - "typescript": "^4.2.0" + "typescript": "^4.8.0" }, "collective": { "type": "opencollective", diff --git a/spec/issues/1935_integer_narrowing_subschema.spec.ts b/spec/issues/1935_integer_narrowing_subschema.spec.ts new file mode 100644 index 000000000..fc24e987b --- /dev/null +++ b/spec/issues/1935_integer_narrowing_subschema.spec.ts @@ -0,0 +1,111 @@ +import _Ajv from "../ajv" +import Ajv from "ajv" +import * as assert from "assert" + +describe("integer valid type in number sub-schema (issue #1935)", () => { + let ajv: Ajv + before(() => { + ajv = new _Ajv({strict: true}) + }) + + it("should allow integer in `if`", () => + assert.doesNotThrow(() => + ajv.compile({ + type: "number", + if: { + type: "integer", + maximum: 5, + }, + else: { + minimum: 10, + }, + }) + )) + + it("should allow integer in `then`", () => + assert.doesNotThrow(() => + ajv.compile({ + type: "number", + if: { + multipleOf: 2, + }, + then: { + type: "integer", + minimum: 10, + }, + }) + )) + + it("should allow integer in `else`", () => + assert.doesNotThrow(() => + ajv.compile({ + type: "number", + if: { + maximum: 5, + }, + else: { + type: "integer", + minimum: 10, + }, + }) + )) + + it("should allow integer in `allOf`", () => + assert.doesNotThrow(() => + ajv.compile({ + type: "number", + allOf: [ + { + type: "integer", + minimum: 10, + }, + { + multipleOf: 2, + }, + ], + }) + )) + + it("should allow integer in `oneOf`", () => + assert.doesNotThrow(() => + ajv.compile({ + type: "number", + oneOf: [ + { + type: "integer", + minimum: 10, + }, + { + multipleOf: 2, + }, + ], + }) + )) + + it("should allow integer in `anyOf`", () => + assert.doesNotThrow(() => + ajv.compile({ + type: "number", + oneOf: [ + { + type: "integer", + minimum: 10, + }, + { + multipleOf: 2, + }, + ], + }) + )) + + it("should allow integer in `not`", () => + assert.doesNotThrow(() => + ajv.compile({ + type: "number", + not: { + type: "integer", + minimum: 10, + }, + }) + )) +}) diff --git a/spec/issues/1949_jtd_empty_values.spec.ts b/spec/issues/1949_jtd_empty_values.spec.ts new file mode 100644 index 000000000..d1ccd7f8f --- /dev/null +++ b/spec/issues/1949_jtd_empty_values.spec.ts @@ -0,0 +1,28 @@ +import _Ajv from "../ajv_jtd" +import * as assert from "assert" + +describe("JTD values with empty schema (issue #1949)", () => { + const ajv = new _Ajv() + + it("should correctly validate empty values form", () => { + const schema = {values: {}} + const validate = ajv.compile(schema) + assert.strictEqual(validate({prop1: 1, prop2: 2}), true) + assert.strictEqual(validate({}), true) + assert.strictEqual(validate(null), false) + assert.strictEqual(validate(1), false) + assert.strictEqual(validate("foo"), false) + assert.strictEqual(validate(undefined), false) + }) + + it("should correctly validate nullable empty values form", () => { + const schema = {values: {}, nullable: true} + const validate = ajv.compile(schema) + assert.strictEqual(validate({prop1: 1, prop2: 2}), true) + assert.strictEqual(validate({}), true) + assert.strictEqual(validate(null), true) + assert.strictEqual(validate(1), false) + assert.strictEqual(validate("foo"), false) + assert.strictEqual(validate(undefined), false) + }) +}) diff --git a/spec/issues/1971_jtd_discriminator.spec.ts b/spec/issues/1971_jtd_discriminator.spec.ts new file mode 100644 index 000000000..80ff57f13 --- /dev/null +++ b/spec/issues/1971_jtd_discriminator.spec.ts @@ -0,0 +1,61 @@ +import _Ajv from "../ajv_jtd" +import * as assert from "assert" + +describe("JTD discriminator with more than 8 (hardcoded in properties.ts) properties (issue #1971)", () => { + const ajv = new _Ajv() + + it("should correctly validate empty values form", () => { + const schema = { + discriminator: "tag", + mapping: { + manual: { + properties: { + first: {type: "uint16"}, + second: {type: "uint16"}, + third: {type: "uint16"}, + fourth: {type: "uint16"}, + fifth: {type: "uint16"}, + sixth: {type: "uint16"}, + additionalOne: {type: "uint16"}, + additionalTwo: {type: "uint16"}, + }, + }, + auto: { + properties: { + first: {type: "uint16"}, + second: {type: "uint16"}, + third: {type: "uint16"}, + fourth: {type: "uint16"}, + fifth: {type: "uint16"}, + sixth: {type: "uint16"}, + additionalThree: {type: "uint16"}, + }, + }, + }, + } + const data1 = { + tag: "manual", + first: 1, + second: 1, + third: 1, + fourth: 1, + fifth: 1, + sixth: 1, + additionalOne: 1, + additionalTwo: 1, + } + const data2 = { + tag: "auto", + first: 1, + second: 1, + third: 1, + fourth: 1, + fifth: 1, + sixth: 1, + additionalThree: 1, + } + const validate = ajv.compile(schema) + assert.strictEqual(validate(data1), true) + assert.strictEqual(validate(data2), true) + }) +}) diff --git a/spec/issues/2001_jtd_only_optional_properties.spec.ts b/spec/issues/2001_jtd_only_optional_properties.spec.ts new file mode 100644 index 000000000..cc7dcfce1 --- /dev/null +++ b/spec/issues/2001_jtd_only_optional_properties.spec.ts @@ -0,0 +1,24 @@ +import _Ajv from "../ajv_jtd" +import * as assert from "assert" + +describe("JTD schema with optional/additional properties only (issue #2001)", () => { + const ajv = new _Ajv() + + it("should correctly serialize optional properties", () => { + const schema = { + optionalProperties: { + prop0: {type: "uint16"}, + prop1: {type: "uint16"}, + prop2: {type: "uint16"}, + }, + additionalProperties: true, + } + const serialize = ajv.compileSerializer(schema) + const test = (data, json) => assert.strictEqual(serialize(data), json) + test({prop0: 0, prop1: 1, prop2: 2}, '{"prop0":0,"prop1":1,"prop2":2}') + test({prop1: 1, prop2: 2}, '{"prop1":1,"prop2":2}') + test({prop0: 0, prop1: 1, prop2: 2, foo: "bar"}, '{"prop0":0,"prop1":1,"prop2":2,"foo":"bar"}') + test({prop1: 1, prop2: 2, foo: "bar"}, '{"prop1":1,"prop2":2,"foo":"bar"}') + test({foo: "bar"}, '{"foo":"bar"}') + }) +}) diff --git a/spec/types/json-schema.spec.ts b/spec/types/json-schema.spec.ts index 26a378083..235548e8e 100644 --- a/spec/types/json-schema.spec.ts +++ b/spec/types/json-schema.spec.ts @@ -306,6 +306,23 @@ describe("JSONSchemaType type and validation as a type guard", () => { // eslint-disable-next-line no-void void optionalSchema }) + + it("won't accept nullable for non-null types", () => { + // @ts-expect-error can't set nullable + const nonNullSchema: JSONSchemaType<{a: number}> = { + type: "object", + properties: { + a: { + type: "number", + nullable: true, + }, + }, + required: [], + } + + // eslint-disable-next-line no-void + void nonNullSchema + }) }) describe("schema works for primitives", () => { diff --git a/spec/types/jtd-schema.spec.ts b/spec/types/jtd-schema.spec.ts index eafb822e1..450577e10 100644 --- a/spec/types/jtd-schema.spec.ts +++ b/spec/types/jtd-schema.spec.ts @@ -1,4 +1,4 @@ -/* eslint-disable @typescript-eslint/no-empty-interface,no-void */ +/* eslint-disable @typescript-eslint/no-empty-interface,no-void,@typescript-eslint/ban-types */ import _Ajv from "../ajv_jtd" import type {JTDSchemaType, SomeJTDSchemaType, JTDDataType} from "../../dist/jtd" import chai from "../chai" @@ -17,8 +17,14 @@ interface B { b?: string } +interface C { + type: "c" +} + type MyData = A | B +type Missing = A | C + interface LinkedList { val: number next?: LinkedList @@ -32,6 +38,14 @@ const mySchema: JTDSchemaType = { }, } +const missingSchema: JTDSchemaType = { + discriminator: "type", + mapping: { + a: {properties: {a: {type: "float64"}}}, + c: {properties: {}}, + }, +} + describe("JTDSchemaType", () => { it("validation should prove the data type", () => { const ajv = new _Ajv() @@ -69,6 +83,22 @@ describe("JTDSchemaType", () => { serialize(invalidData) }) + it("validation should prove the data type for missingSchema", () => { + const ajv = new _Ajv() + const validate = ajv.compile(missingSchema) + const validData: unknown = {type: "c"} + + if (validate(validData)) { + validData.type.should.equal("c") + } + should.not.exist(validate.errors) + + if (ajv.validate(missingSchema, validData)) { + validData.type.should.equal("c") + } + should.not.exist(validate.errors) + }) + it("should typecheck number schemas", () => { const numf: JTDSchemaType = {type: "float64"} const numi: JTDSchemaType = {type: "int32"} @@ -130,12 +160,12 @@ describe("JTDSchemaType", () => { // tuples don't work // @ts-expect-error const tupleHomo: JTDSchemaType<[number, number]> = {elements: {type: "float64"}} + // @ts-expect-error const tupleHeteroNum: JTDSchemaType<[number, string]> = { - // @ts-expect-error elements: {type: "float64"}, } + // @ts-expect-error const tupleHeteroString: JTDSchemaType<[number, string]> = { - // @ts-expect-error elements: {type: "string"}, } const elemNull: JTDSchemaType = {elements: {type: "float64"}, nullable: true} @@ -286,12 +316,42 @@ describe("JTDSchemaType", () => { const emptyButFull: JTDSchemaType<{a: string}> = {} const emptyMeta: JTDSchemaType = {metadata: {}} - // constant null not representable + // constant null representable as nullable empty object const emptyNull: TypeEquality, never> = true void [empty, emptyUnknown, falseUnknown, emptyButFull, emptyMeta, emptyNull] }) + it("should typecheck empty records", () => { + // empty record variants + const emptyPro: JTDSchemaType<{}> = {properties: {}} + const emptyOpt: JTDSchemaType<{}> = {optionalProperties: {}} + const emptyBoth: JTDSchemaType<{}> = {properties: {}, optionalProperties: {}} + const emptyRecord: JTDSchemaType> = {properties: {}} + const notNullable: JTDSchemaType<{}> = {properties: {}, nullable: false} + + // can't be null + // @ts-expect-error + const nullable: JTDSchemaType<{}> = {properties: {}, nullable: true} + + const emptyNullUnion: JTDSchemaType = {properties: {}, nullable: true} + const emptyNullRecord: JTDSchemaType> = { + properties: {}, + nullable: true, + } + + void [ + emptyPro, + emptyOpt, + emptyBoth, + emptyRecord, + notNullable, + nullable, + emptyNullUnion, + emptyNullRecord, + ] + }) + it("should typecheck ref schemas", () => { const refs: JTDSchemaType = { definitions: {