Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: ajv-validator/ajv
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v8.3.0
Choose a base ref
...
head repository: ajv-validator/ajv
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v8.4.0
Choose a head ref
  • 4 commits
  • 11 files changed
  • 2 contributors

Commits on May 14, 2021

  1. JTD timestamps: parseDate, allowDate options (#1609)

    * JTD timestamps: parseDate, allowDate options
    
    * fix prettier layout, update node versions
    
    * change node versions back
    epoberezkin authored May 14, 2021
    Copy the full SHA
    de021c4 View commit details
  2. Ajv in Internet Explorer 11 FAQs documentation (#1594)

    * Ajv in Internet Explorer 11 FAQs documentation
    
    * Revert "Ajv in Internet Explorer 11 FAQs documentation"
    
    This reverts commit c726884.
    
    * Ajv IE11 documentation in environments.md
    
    Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
    mummybot and epoberezkin authored May 14, 2021

    Unverified

    The email in this signature doesn’t match the committer email.
    Copy the full SHA
    4322e7b View commit details
  3. set test timeout (#1610)

    epoberezkin authored May 14, 2021

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    67e1bd3 View commit details
  4. 8.4.0

    epoberezkin committed May 14, 2021

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    84abab2 View commit details
Showing with 148 additions and 78 deletions.
  1. +1 −1 docs/api.md
  2. +3 −0 docs/guide/environments.md
  3. +24 −5 docs/options.md
  4. +8 −4 lib/compile/jtd/parse.ts
  5. +4 −3 lib/core.ts
  6. +3 −2 lib/runtime/quote.ts
  7. +29 −12 lib/runtime/timestamp.ts
  8. +9 −16 lib/vocabularies/jtd/type.ts
  9. +2 −2 package.json
  10. +61 −25 spec/jtd-timestamps.spec.ts
  11. +4 −8 spec/types/jtd-schema.spec.ts
2 changes: 1 addition & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
@@ -331,7 +331,7 @@ In case of validation failure, Ajv assigns the array of errors to `errors` prope

### Error objects

Each error reported when validating against JSON Schema (also when validating against JTD schema with option `ajvErrors`) is an object with the following properties:
Each reported error is an object with the following properties:

```typescript
interface ErrorObject {
3 changes: 3 additions & 0 deletions docs/guide/environments.md
Original file line number Diff line number Diff line change
@@ -77,6 +77,9 @@ The browser bundles are available on [cdnjs](https://cdnjs.com/libraries/ajv).
Some frameworks, e.g. Dojo, may redefine global require in a way that is not compatible with CommonJS module format. In this case Ajv bundle has to be loaded before the framework and then you can use global `ajv` (see issue [#234](https://github.com/ajv-validator/ajv/issues/234)).
:::

::: warning Ajv v8 in Internet Explorer 11 (IE11) will not work straight out of the box. To use it either [recompile it](https://ajv.js.org/standalone.html), or set the options [unicodeRegExp](https://ajv.js.org/options.html#unicoderegexp) to `false` and `code: { es5: true }`, and transpile the Ajv node module (see issue [#1585](https://github.com/ajv-validator/ajv/issues/1585#issuecomment-832486204)).
:::

## ES5 environments

You need to:
29 changes: 24 additions & 5 deletions docs/options.md
Original file line number Diff line number Diff line change
@@ -37,6 +37,9 @@ const defaultOptions = {
verbose: false,
discriminator: false, // *
unicodeRegExp: true, // *
timestamp: undefined // **
parseDate: false // **
allowDate: false // **
$comment: false, // *
formats: {},
keywords: {},
@@ -69,7 +72,9 @@ const defaultOptions = {
}
```

<sup>\*</sup> these options are not supported with JSON Type Definition schemas
<sup>\*</sup> only with JSON Schema

<sup>\**</sup> only with JSON Type Definition

## Strict mode options <Badge text="v7" />

@@ -177,6 +182,24 @@ Option values:
- `true` (default) - use unicode flag "u".
- `false` - do not use flag "u".

### timestamp <Badge text="JTD only">

Defines which Javascript types will be accepted for the [JTD timestamp type](./json-type-definition#type-form).

By default Ajv will accept both Date objects and [RFC3339](https://datatracker.ietf.org/doc/rfc3339/) strings. You can specify allowed values with the option `timestamp: "date"` or `timestamp: "string"`.

### parseDate <Badge text="JTD only">

Defines how date-time strings are parsed by [JTD parsers](./api.md#jtd-parse). By default Ajv parses date-time strings as string. Use `parseDate: true` to parse them as Date objects.

### allowDate <Badge text="JTD only">

Defines how date-time strings are parsed and validated. By default Ajv only allows full date-time strings, as required by JTD specification. Use `allowDate: true` to allow date strings both for validation and for parsing.

::: warning Option allowDate is not portable
This option makes JTD validation and parsing more permissive and non-standard. The date strings without time part will be accepted by Ajv, but will be rejected by other JTD validators.
:::

### $comment

Log or pass the value of `$comment` keyword to a function.
@@ -212,10 +235,6 @@ Option values:

Asynchronous function that will be used to load remote schemas when `compileAsync` [method](#api-compileAsync) is used and some reference is missing (option `missingRefs` should NOT be 'fail' or 'ignore'). This function should accept remote schema uri as a parameter and return a Promise that resolves to a schema. See example in [Asynchronous compilation](./guide/managing-schemas.md#asynchronous-schema-compilation).

### timestamp

(JTD only) This governs what Javascript types will be accepted for the [JTD timestamp type](./json-type-definition#type-form). By default Ajv will accept either Date objects or [RFC3339](https://datatracker.ietf.org/doc/rfc3339/) strings. You can adjust this behavior by specifying `timestamp: "date"` or `timestamp: "string"`.

## Options to modify validated data

### removeAdditional
12 changes: 8 additions & 4 deletions lib/compile/jtd/parse.ts
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import type Ajv from "../../core"
import type {SchemaObject} from "../../types"
import {jtdForms, JTDForm, SchemaObjectMap} from "./types"
import {SchemaEnv, getCompilingSchema} from ".."
import {_, str, and, nil, not, CodeGen, Code, Name, SafeExpr} from "../codegen"
import {_, str, and, or, nil, not, CodeGen, Code, Name, SafeExpr} from "../codegen"
import MissingRefError from "../ref_error"
import N from "../names"
import {hasPropFunc} from "../../vocabularies/code"
@@ -253,7 +253,7 @@ function parsePropertyValue(cxt: ParseCxt, key: Name, schema: SchemaObject): voi
}

function parseType(cxt: ParseCxt): void {
const {gen, schema, data} = cxt
const {gen, schema, data, self} = cxt
switch (schema.type) {
case "boolean":
parseBoolean(cxt)
@@ -262,10 +262,14 @@ function parseType(cxt: ParseCxt): void {
parseString(cxt)
break
case "timestamp": {
// TODO parse timestamp?
parseString(cxt)
const vts = useFunc(gen, validTimestamp)
gen.if(_`!${vts}(${data})`, () => parsingError(cxt, str`invalid timestamp`))
const {allowDate, parseDate} = self.opts
const notValid = allowDate ? _`!${vts}(${data}, true)` : _`!${vts}(${data})`
const fail: Code = parseDate
? or(notValid, _`(${data} = new Date(${data}), false)`, _`isNaN(${data}.valueOf())`)
: notValid
gen.if(fail, () => parsingError(cxt, str`invalid timestamp`))
break
}
case "float32":
7 changes: 4 additions & 3 deletions lib/core.ts
Original file line number Diff line number Diff line change
@@ -98,6 +98,9 @@ export interface CurrentOptions {
verbose?: boolean
discriminator?: boolean
unicodeRegExp?: boolean
timestamp?: "string" | "date" // JTD only
parseDate?: boolean // JTD only
allowDate?: boolean // JTD only
$comment?:
| true
| ((comment: string, schemaPath?: string, rootSchema?: AnySchemaObject) => unknown)
@@ -115,8 +118,6 @@ export interface CurrentOptions {
unevaluated?: boolean // NEW
dynamicRef?: boolean // NEW
jtd?: boolean // NEW
/** (JTD only) Accepted Javascript types for `timestamp` type */
timestamp?: "string" | "date"
meta?: SchemaObject | boolean
defaultMeta?: string | AnySchemaObject
validateSchema?: boolean | "log"
@@ -192,7 +193,7 @@ const removedOptions: OptionsInfo<RemovedOptions> = {
unknownFormats: "Disable strict mode or pass `true` to `ajv.addFormat` (or `formats` option).",
cache: "Map is used as cache, schema object as key.",
serialize: "Map is used as cache, schema object as key.",
ajvErrors: "It is default now, see option `strict`.",
ajvErrors: "It is default now.",
}

const deprecatedOptions: OptionsInfo<DeprecatedOptions> = {
5 changes: 3 additions & 2 deletions lib/runtime/quote.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// eslint-disable-next-line no-control-regex, no-misleading-character-class
const rxEscapable = /[\\"\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g
const rxEscapable =
// eslint-disable-next-line no-control-regex, no-misleading-character-class
/[\\"\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g

const escaped: {[K in string]?: string} = {
"\b": "\\b",
41 changes: 29 additions & 12 deletions lib/runtime/timestamp.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,45 @@
const DATE_TIME = /^(\d\d\d\d)-(\d\d)-(\d\d)(?:t|\s)(\d\d):(\d\d):(\d\d)(?:\.\d+)?(?:z|([+-]\d\d)(?::?(\d\d))?)$/i
const DT_SEPARATOR = /t|\s/i
const DATE = /^(\d\d\d\d)-(\d\d)-(\d\d)$/
const TIME = /^(\d\d):(\d\d):(\d\d)(?:\.\d+)?(?:z|([+-]\d\d)(?::?(\d\d))?)$/i
const DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]

export default function validTimestamp(str: string): boolean {
export default function validTimestamp(str: string, allowDate: boolean): boolean {
// http://tools.ietf.org/html/rfc3339#section-5.6
const matches: string[] | null = DATE_TIME.exec(str)
const dt: string[] = str.split(DT_SEPARATOR)
return (
(dt.length === 2 && validDate(dt[0]) && validTime(dt[1])) ||
(allowDate && dt.length === 1 && validDate(dt[0]))
)
}

function validDate(str: string): boolean {
const matches: string[] | null = DATE.exec(str)
if (!matches) return false
const y: number = +matches[1]
const m: number = +matches[2]
const d: number = +matches[3]
const hr: number = +matches[4]
const min: number = +matches[5]
const sec: number = +matches[6]
const tzH: number = +(matches[7] || 0)
const tzM: number = +(matches[8] || 0)
return (
m >= 1 &&
m <= 12 &&
d >= 1 &&
(d <= DAYS[m] ||
// leap year: https://tools.ietf.org/html/rfc3339#appendix-C
(m === 2 && d === 29 && (y % 100 === 0 ? y % 400 === 0 : y % 4 === 0))) &&
((hr <= 23 && min <= 59 && sec <= 59) ||
// leap second
(hr - tzH === 23 && min - tzM === 59 && sec === 60))
(m === 2 && d === 29 && (y % 100 === 0 ? y % 400 === 0 : y % 4 === 0)))
)
}

function validTime(str: string): boolean {
const matches: string[] | null = TIME.exec(str)
if (!matches) return false
const hr: number = +matches[1]
const min: number = +matches[2]
const sec: number = +matches[3]
const tzH: number = +(matches[4] || 0)
const tzM: number = +(matches[5] || 0)
return (
(hr <= 23 && min <= 59 && sec <= 59) ||
// leap second
(hr - tzH === 23 && min - tzM === 59 && sec === 60)
)
}

25 changes: 9 additions & 16 deletions lib/vocabularies/jtd/type.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import type {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types"
import type {KeywordCxt} from "../../compile/validate"
import {_, or, Code} from "../../compile/codegen"
import {_, nil, or, Code} from "../../compile/codegen"
import validTimestamp from "../../runtime/timestamp"
import {useFunc} from "../../compile/util"
import {checkMetadata} from "./metadata"
import {typeErrorMessage, typeErrorParams, _JTDTypeError} from "./error"
import {_Code} from "../../compile/codegen/code"

export type JTDTypeError = _JTDTypeError<"type", JTDType, JTDType>

@@ -27,20 +26,14 @@ const error: KeywordErrorDefinition = {
params: (cxt) => typeErrorParams(cxt, cxt.schema),
}

function timestampCode(cxt: KeywordCxt): _Code {
const {gen, data} = cxt
switch (cxt.it.opts.timestamp) {
case "date":
return _`${data} instanceof Date `
case "string": {
const vts = useFunc(gen, validTimestamp)
return _`typeof ${data} == "string" && ${vts}(${data})`
}
default: {
const vts = useFunc(gen, validTimestamp)
return _`${data} instanceof Date || (typeof ${data} == "string" && ${vts}(${data}))`
}
}
function timestampCode(cxt: KeywordCxt): Code {
const {gen, data, it} = cxt
const {timestamp, allowDate} = it.opts
if (timestamp === "date") return _`${data} instanceof Date `
const vts = useFunc(gen, validTimestamp)
const allowDateArg = allowDate ? _`, true` : nil
const validString = _`typeof ${data} == "string" && ${vts}(${data}${allowDateArg})`
return timestamp === "string" ? validString : or(_`${data} instanceof Date`, validString)
}

const def: CodeKeywordDefinition = {
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ajv",
"version": "8.3.0",
"version": "8.4.0",
"description": "Another JSON Schema Validator",
"main": "dist/ajv.js",
"types": "dist/ajv.d.ts",
@@ -88,7 +88,7 @@
"jimp": "^0.16.1",
"js-beautify": "^1.7.3",
"json-schema-test": "^2.0.0",
"karma": "^5.0.0",
"karma": "^6.0.0",
"karma-chrome-launcher": "^3.0.0",
"karma-mocha": "^2.0.0",
"lint-staged": "^10.2.11",
86 changes: 61 additions & 25 deletions spec/jtd-timestamps.spec.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,70 @@
import _AjvJTD from "./ajv_jtd"
import assert = require("assert")
import type {JTDOptions, JTDSchemaType} from "../dist/jtd"

describe("JTD Timestamps", () => {
it("Should accept dates or strings by default", () => {
const ajv = new _AjvJTD()
const schema = {
type: "timestamp",
}
assert.strictEqual(ajv.validate(schema, new Date()), true)
assert.strictEqual(ajv.validate(schema, "2021-05-03T05:24:43.906Z"), true)
assert.strictEqual(ajv.validate(schema, "foo"), false)
})
describe("JTD timestamps", function () {
this.timeout(10000)

describe("validation", () => {
it("should accept dates or strings by default", () => {
testTimestamp({}, {Date: true, datetime: true, date: false})
})

it("timestamp: string should accept only strings", () => {
testTimestamp({timestamp: "string"}, {Date: false, datetime: true, date: false})
})

it("Should enforce timestamp=string", () => {
const ajv = new _AjvJTD({timestamp: "string"})
const schema = {
type: "timestamp",
it("timestamp: date should accept only Date objects", () => {
testTimestamp({timestamp: "date"}, {Date: true, datetime: false, date: false})
})

it("allowDate: true should accept date without time component", () => {
testTimestamp({allowDate: true}, {Date: true, datetime: true, date: true})
testTimestamp(
{allowDate: true, timestamp: "string"},
{Date: false, datetime: true, date: true}
)
testTimestamp(
{allowDate: true, timestamp: "date"},
{Date: true, datetime: false, date: false}
)
})

function testTimestamp(
opts: JTDOptions,
valid: {Date: boolean; datetime: boolean; date: boolean}
) {
const ajv = new _AjvJTD(opts)
const schema = {type: "timestamp"}
const validate = ajv.compile(schema)
assert.strictEqual(validate(new Date()), valid.Date)
assert.strictEqual(validate("2021-05-03T05:24:43.906Z"), valid.datetime)
assert.strictEqual(validate("2021-05-03"), valid.date)
assert.strictEqual(validate("foo"), false)
}
assert.strictEqual(ajv.validate(schema, new Date()), false)
assert.strictEqual(ajv.validate(schema, "2021-05-03T05:24:43.906Z"), true)
assert.strictEqual(ajv.validate(schema, "foo"), false)
})

it("Should enforce timestamp=date", () => {
const ajv = new _AjvJTD({timestamp: "date"})
const schema = {
type: "timestamp",
}
assert.strictEqual(ajv.validate(schema, new Date()), true)
assert.strictEqual(ajv.validate(schema, "2021-05-03T05:24:43.906Z"), false)
assert.strictEqual(ajv.validate(schema, "foo"), false)
describe("parseDate option", () => {
it("should parse timestamp as Date object", () => {
const schema: JTDSchemaType<Date> = {type: "timestamp"}
const ajv = new _AjvJTD({parseDate: true})
const parseTS = ajv.compileParser(schema)
assert.strictEqual(
parseTS('"2021-05-14T17:59:03.851Z"')?.toISOString(),
"2021-05-14T17:59:03.851Z"
)
assert.strictEqual(parseTS('"2021-05-14"')?.toISOString(), undefined)
})

it("allowDate: true should parse timestamp and date as Date objects", () => {
const schema: JTDSchemaType<Date> = {type: "timestamp"}
const ajv = new _AjvJTD({parseDate: true, allowDate: true})
const parseTS = ajv.compileParser(schema)
assert.strictEqual(
parseTS('"2021-05-14T17:59:03.851Z"')?.toISOString(),
"2021-05-14T17:59:03.851Z"
)
assert.strictEqual(parseTS('"2021-05-14"')?.toISOString(), "2021-05-14T00:00:00.000Z")
})
})
})
12 changes: 4 additions & 8 deletions spec/types/jtd-schema.spec.ts
Original file line number Diff line number Diff line change
@@ -173,10 +173,8 @@ describe("JTDSchemaType", () => {
// can typecheck a values of unions
const unionValues: TypeEquality<JTDSchemaType<Record<string, A | B>>, never> = false
// can't typecheck a union of values
const valuesUnion: TypeEquality<
JTDSchemaType<Record<string, A> | Record<string, B>>,
never
> = true
const valuesUnion: TypeEquality<JTDSchemaType<Record<string, A> | Record<string, B>>, never> =
true

void [values, readonlyValues, valuesDefined, valuesNull, unionValues, valuesUnion]
})
@@ -438,10 +436,8 @@ describe("JTDDataType", () => {
optionalProperties: {b: {type: "string"}},
additionalProperties: true,
} as const
const add: TypeEquality<
JTDDataType<typeof addSchema>,
{b?: string; [key: string]: unknown}
> = true
const add: TypeEquality<JTDDataType<typeof addSchema>, {b?: string; [key: string]: unknown}> =
true
const addVal: JTDDataType<typeof addSchema> = {b: "b", additional: 6}

void [both, req, opt, noAdd, add, addVal]