Skip to content

Commit

Permalink
Merge branch 'master' into discriminator
Browse files Browse the repository at this point in the history
  • Loading branch information
pkuczynski committed Apr 6, 2023
2 parents 813f8cf + 45583fd commit 364dfde
Show file tree
Hide file tree
Showing 16 changed files with 383 additions and 60 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Expand Up @@ -12,7 +12,7 @@ jobs:

strategy:
matrix:
node-version: [12.x, 14.x, 16.x]
node-version: [14.x, 16.x, 18.x, 19.x]

steps:
- uses: actions/checkout@v2
Expand Down
9 changes: 5 additions & 4 deletions README.md
Expand Up @@ -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-%2307b4b9)](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FV-6t4hoy_SsvKMi9KekdGX-VKQOhDeAe%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAm98gjwvrAEiiz_YgBoaQB9dtKTl5Om1pborUyevQwzg%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22wYrTFafovkymjUtc2vUjCQ%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)

Expand All @@ -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)
Expand Down Expand Up @@ -100,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

Expand Down Expand Up @@ -157,15 +158,15 @@ const schema = {
type: "object",
properties: {
foo: {type: "integer"},
bar: {type: "string"}
bar: {type: "string"},
},
required: ["foo"],
additionalProperties: false,
}

const data = {
foo: 1,
bar: "abc"
bar: "abc",
}

const validate = ajv.compile(schema)
Expand Down
17 changes: 10 additions & 7 deletions docs/guide/schema-language.md
Expand Up @@ -2,6 +2,7 @@
tags:
- JTD
---

# Choosing schema language

[[toc]]
Expand Down Expand Up @@ -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<sup>\*</sup>, conditionals, references between different schema files<sup>\*\*</sup>, etc.
- No meta-schema in the specification<sup>\*</sup>.
- Brand new - limited industry adoption (as of January 2021).
- Limited, compared with JSON Schema - no support for untagged unions<sup>1</sup>, conditionals, references between different schema files<sup>2</sup>, etc.
- No meta-schema in the specification<sup>3</sup>.

<sup>1</sup> Ajv defines non-standard keyword "union" that can be used inside "metadata" object.

<sup>\*</sup> Ajv defines meta-schema for JTD schemas and non-standard keyword "union" that can be used inside "metadata" object.
<sup>2</sup> You can still combine schemas from multiple files in the application code.

<sup>\*\*</sup> You can still combine schemas from multiple files in the application code.
<sup>3</sup> 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.
34 changes: 20 additions & 14 deletions lib/compile/jtd/serialize.ts
Expand Up @@ -117,7 +117,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})
Expand Down Expand Up @@ -156,20 +156,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))
)
}

Expand All @@ -190,8 +194,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})
}
Expand Down Expand Up @@ -251,10 +253,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`,`)
}
}
2 changes: 1 addition & 1 deletion lib/compile/jtd/types.ts
Expand Up @@ -13,4 +13,4 @@ export const jtdForms = [
"ref",
] as const

export type JTDForm = typeof jtdForms[number]
export type JTDForm = (typeof jtdForms)[number]
2 changes: 1 addition & 1 deletion lib/compile/rules.ts
Expand Up @@ -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<string> = new Set(_jsonTypes)

Expand Down
11 changes: 10 additions & 1 deletion lib/compile/validate/index.ts
Expand Up @@ -286,7 +286,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 {
Expand Down Expand Up @@ -316,6 +316,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)`
Expand Down
32 changes: 21 additions & 11 deletions lib/types/jtd-schema.ts
Expand Up @@ -74,7 +74,7 @@ type EnumString<T> = [T] extends [never]
: null

/** true if type is a union of string literals */
type IsEnum<T> = null extends EnumString<Exclude<T, null>> ? false : true
type IsEnum<T> = null extends EnumString<T> ? 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
Expand All @@ -88,13 +88,18 @@ type IsElements<T> = false extends IsUnion<T>
: false

/** true if the the type is a values type */
type IsValues<T> = false extends IsUnion<Exclude<T, null>>
? TypeEquality<keyof Exclude<T, null>, string>
type IsValues<T> = false extends IsUnion<T> ? TypeEquality<keyof T, string> : false

/** true if type is a properties type and Union is false, or type is a discriminator type and Union is true */
type IsRecord<T, Union extends boolean> = Union extends IsUnion<T>
? null extends EnumString<keyof T>
? 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<T, Union extends boolean> = Union extends IsUnion<Exclude<T, null>>
? null extends EnumString<keyof Exclude<T, null>>
/** true if type represents an empty record */
type IsEmptyRecord<T> = [T] extends [Record<string, never>]
? [T] extends [never]
? false
: true
: false
Expand Down Expand Up @@ -131,7 +136,7 @@ export type JTDSchemaType<T, D extends Record<string, unknown> = Record<string,
? {type: "timestamp"}
: // enums - only accepts union of string literals
// TODO we can't actually check that everything in the union was specified
true extends IsEnum<T>
true extends IsEnum<Exclude<T, null>>
? {enum: EnumString<Exclude<T, null>>[]}
: // arrays - only accepts arrays, could be array of unions to be resolved later
true extends IsElements<Exclude<T, null>>
Expand All @@ -140,15 +145,20 @@ export type JTDSchemaType<T, D extends Record<string, unknown> = Record<string,
elements: JTDSchemaType<E, D>
}
: never
: // empty properties
true extends IsEmptyRecord<Exclude<T, null>>
?
| {properties: Record<string, never>; optionalProperties?: Record<string, never>}
| {optionalProperties: Record<string, never>}
: // values
true extends IsValues<T>
true extends IsValues<Exclude<T, null>>
? T extends Record<string, infer V>
? {
values: JTDSchemaType<V, D>
}
: never
: // properties
true extends IsRecord<T, false>
true extends IsRecord<Exclude<T, null>, false>
? ([RequiredKeys<Exclude<T, null>>] extends [never]
? {
properties?: Record<string, never>
Expand All @@ -168,15 +178,15 @@ export type JTDSchemaType<T, D extends Record<string, unknown> = Record<string,
additionalProperties?: boolean
}
: // discriminator
true extends IsRecord<T, true>
true extends IsRecord<Exclude<T, null>, true>
? {
[K in keyof Exclude<T, null>]-?: Exclude<T, null>[K] extends string
? {
discriminator: K
mapping: {
// TODO currently allows descriminator to be present in schema
[M in Exclude<T, null>[K]]: JTDSchemaType<
Omit<T extends {[C in K]: M} ? T : never, K>,
Omit<T extends Record<K, M> ? T : never, K>,
D
>
}
Expand Down
19 changes: 13 additions & 6 deletions lib/vocabularies/jtd/properties.ts
Expand Up @@ -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)
Expand All @@ -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
}
Expand Down
17 changes: 10 additions & 7 deletions 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"
Expand All @@ -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 {
Expand Down
10 changes: 5 additions & 5 deletions package.json
@@ -1,6 +1,6 @@
{
"name": "ajv",
"version": "8.11.0",
"version": "8.12.0",
"description": "Another JSON Schema Validator",
"main": "dist/ajv.js",
"types": "dist/ajv.d.ts",
Expand Down Expand Up @@ -65,12 +65,12 @@
},
"devDependencies": {
"@ajv-validator/config": "^0.3.0",
"@rollup/plugin-commonjs": "^23.0.2",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-commonjs": "^24.0.0",
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-typescript": "^8.2.1",
"@rollup/plugin-typescript": "^10.0.1",
"@types/chai": "^4.2.12",
"@types/mocha": "^9.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",
Expand Down

0 comments on commit 364dfde

Please sign in to comment.