Skip to content
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:

strategy:
matrix:
node-version: [10.x, 12.x, 14.x]
node-version: [16.x, 18.x, 20.x, 21.x]

steps:
- uses: actions/checkout@v2
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ These keywords are added to ajv instance when ajv-formats is used without option

These keywords apply only to strings. If the data is not a string, the validation succeeds.

The value of keywords `formatMaximum`/`formatMinimum` and `formatExclusiveMaximum`/`formatExclusiveMinimum` should be a string or [\$data reference](https://github.com/ajv-validator/ajv/blob/master/docs/validation.md#data-reference). This value is the maximum (minimum) allowed value for the data to be valid as determined by `format` keyword. If `format` keyword is not present schema compilation will throw exception.
The value of keywords `formatMaximum`/`formatMinimum` and `formatExclusiveMaximum`/`formatExclusiveMinimum` should be a string or [$data reference](https://github.com/ajv-validator/ajv/blob/master/docs/guide/combining-schemas.md#data-reference). This value is the maximum (minimum) allowed value for the data to be valid as determined by `format` keyword. If `format` keyword is not present schema compilation will throw exception.

When these keyword are added, they also add comparison functions to formats `"date"`, `"time"` and `"date-time"`. User-defined formats also can have comparison functions. See [addFormat](https://github.com/ajv-validator/ajv/blob/master/docs/api.md#api-addformat) method.

Expand Down Expand Up @@ -95,7 +95,7 @@ addFormats(ajv, ["date", "time"])
**Please note**: when ajv encounters an undefined format it throws exception (unless ajv instance was configured with `strict: false` option). To allow specific undefined formats they have to be passed to ajv instance via `formats` option with `true` value:

```javascript
const ajv = new Ajv((formats: {date: true, time: true})) // to ignore "date" and "time" formats in schemas.
const ajv = new Ajv({formats: {date: true, time: true}}) // to ignore "date" and "time" formats in schemas.
```

2. Format validation mode (default is `"full"`) with optional list of format names and `keywords` option to add additional format comparison keywords:
Expand Down
21 changes: 11 additions & 10 deletions src/formats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ export const fullFormats: DefinedFormats = {
date: fmtDef(date, compareDate),
// date-time: http://tools.ietf.org/html/rfc3339#section-5.6
time: fmtDef(getTime(true), compareTime),
"date-time": fmtDef(getDateTime(true), compareDateTime),
"date-time": fmtDef(getDateTime(), compareDateTime),
"iso-time": fmtDef(getTime(), compareIsoTime),
"iso-date-time": fmtDef(getDateTime(), compareIsoDateTime),
"iso-date-time": fmtDef(getDateTime(true), compareIsoDateTime),
// duration: https://tools.ietf.org/html/rfc3339#appendix-A
duration: /^P(?!$)((\d+Y)?(\d+M)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?|(\d+W)?)$/,
uri,
Expand Down Expand Up @@ -102,15 +102,15 @@ export const fastFormats: DefinedFormats = {
compareTime
),
"date-time": fmtDef(
/^\d\d\d\d-[0-1]\d-[0-3]\dt(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/i,
/^\d\d\d\d-((0[1-9])|(1[0-2]))-((0[1-9])|([1-2]\d)|(3[01]))[tT](?:(([0-1]\d)|(2[0-3])):[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:[zZ]|[+-](([0-1]\d)|(2[0-3])):[0-5]\d)$/,
compareDateTime
),
"iso-time": fmtDef(
/^(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)?$/i,
compareIsoTime
),
"iso-date-time": fmtDef(
/^\d\d\d\d-[0-1]\d-[0-3]\d[t\s](?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)?$/i,
/^\d\d\d\d-((0[1-9])|(1[0-2]))-((0[1-9])|([1-2]\d)|(3[01]))[t\s](?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)?$/i,
compareIsoDateTime
),
// uri: https://github.com/mafintosh/is-my-json-valid/blob/master/formats.js
Expand Down Expand Up @@ -197,13 +197,14 @@ function compareIsoTime(t1: string, t2: string): number | undefined {
return 0
}

const DATE_TIME_SEPARATOR = /t|\s/i
function getDateTime(strictTimeZone?: boolean): (str: string) => boolean {
const time = getTime(strictTimeZone)
const DATE_TIME_SEPARATOR = /t/i
const ISO_DATE_TIME_SEPARATOR = /t|\s/i
function getDateTime(iso?: boolean): (str: string) => boolean {
const time = getTime(!iso)

return function date_time(str: string): boolean {
// http://tools.ietf.org/html/rfc3339#section-5.6
const dateTime: string[] = str.split(DATE_TIME_SEPARATOR)
const dateTime: string[] = str.split(iso ? ISO_DATE_TIME_SEPARATOR : DATE_TIME_SEPARATOR)
return dateTime.length === 2 && date(dateTime[0]) && time(dateTime[1])
}
}
Expand All @@ -218,8 +219,8 @@ function compareDateTime(dt1: string, dt2: string): number | undefined {

function compareIsoDateTime(dt1: string, dt2: string): number | undefined {
if (!(dt1 && dt2)) return undefined
const [d1, t1] = dt1.split(DATE_TIME_SEPARATOR)
const [d2, t2] = dt2.split(DATE_TIME_SEPARATOR)
const [d1, t1] = dt1.split(ISO_DATE_TIME_SEPARATOR)
const [d2, t2] = dt2.split(ISO_DATE_TIME_SEPARATOR)
const res = compareDate(d1, d2)
if (res === undefined) return undefined
return res || compareTime(t1, t2)
Expand Down
128 changes: 127 additions & 1 deletion tests/extras/format.json
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,7 @@
]
},
{
"description": "validation of date-time strings",
"description": "validation of iso-date-time strings",
"schema": {"format": "iso-date-time"},
"tests": [
{
Expand Down Expand Up @@ -597,6 +597,132 @@
"description": "a valid iso-date-time string (leap second)",
"data": "2016-12-31T23:59:60Z",
"valid": true
},
{
"description": "a valid iso-date-time string (space separator)",
"data": "2016-12-31 23:59:60Z",
"valid": true
}
]
},
{
"description": "validation of date-time strings",
"schema": {"format": "date-time"},
"tests": [
{
"description": "a valid date-time string",
"data": "2016-12-31T23:59:60Z",
"valid": true
},
{
"description": "a valid date-time string",
"data": "2015-12-31t23:59:60Z",
"valid": true
},
{
"description": "a valid date-time string",
"data": "2015-02-11t22:59:22Z",
"valid": true
},
{
"description": "a valid date-time string",
"data": "2020-01-01T20:00:00.000Z",
"valid": true
},
{
"description": "a valid date-time string",
"data": "2020-01-01T20:00:00.000-00:00",
"valid": true
},
{
"description": "a valid date-time string",
"data": "2023-05-04T01:14:00+21:00",
"valid": true
},
{
"description": "a valid date-time string",
"data": "2023-05-04T01:14:10+16:20",
"valid": true
},
{
"description": "a valid date-time string",
"data": "2023-05-04T01:14:21+09:50",
"valid": true
},
{
"description": "a valid date-time string",
"data": "2023-05-04T01:14:21-04:31",
"valid": true
},
{
"description": "a valid date-time string",
"data": "2023-05-04T01:14:21-23:59",
"valid": true
},
{
"description": "an invalid date-time string (invalid month)",
"data": "2016-15-31T23:59:60Z",
"valid": false
},
{
"description": "an invalid date-time string (invalid month)",
"data": "2015-00-11t22:59:22+00:00",
"valid": false
},
{
"description": "an invalid date-time string (invalid day)",
"data": "2015-01-00T22:59:22+00:00",
"valid": false
},
{
"description": "an invalid date-time string (invalid separator)",
"data": "2016-12-31 23:59:60Z",
"valid": false
},
{
"description": "an invalid date-time string (invalid time)",
"data": "2015-02-11t24:59:22Z",
"valid": false
},
{
"description": "an invalid date-time string (invalid separator and time-offset)",
"data": "2020-01-01 20:00:00.000",
"valid": false
},
{
"description": "an invalid date-time string (invalid separator)",
"data": "2020-01-01 20:00:00.000Z",
"valid": false
},
{
"description": "an invalid date-time string (invalid separator)",
"data": "2023-05-04\t01:14:00+21:00",
"valid": false
},
{
"description": "an invalid date-time string (invalid separator)",
"data": "2023-05-04\r01:14:10+16:20",
"valid": false
},
{
"description": "an invalid date-time string (invalid timezone)",
"data": "2015-02-11t22:59:22+24:30",
"valid": false
},
{
"description": "an invalid date-time string (invalid separator)",
"data": "2023-05-04\n01:14:21+09:50",
"valid": false
},
{
"description": "an invalid date-time string (invalid separator)",
"data": "2023-05-04\n01:14:21-04:31",
"valid": false
},
{
"description": "an invalid date-time string (invalid time-offset)",
"data": "2023-05-04t01:14:21-04:31:00",
"valid": false
}
]
},
Expand Down
31 changes: 30 additions & 1 deletion tests/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ describe("addFormats options", () => {
})

test("should support validation mode", () => {
addFormats(ajv, {mode: "fast", formats: ["date", "time"]})
addFormats(ajv, {mode: "fast", formats: ["date", "time", "date-time", "iso-date-time"]})
const validateDate = ajv.compile({format: "date"})
expect(validateDate("2020-09-17")).toEqual(true)
expect(validateDate("2020-09-35")).toEqual(true)
Expand All @@ -35,6 +35,35 @@ describe("addFormats options", () => {
expect(validateTime("17:27:38Z")).toEqual(true)
expect(validateTime("25:27:38Z")).toEqual(true)
expect(validateTime("17:27")).toEqual(false)

const validateDatetime = ajv.compile({format: "date-time"})
expect(validateDatetime("2016-12-31T23:59:60Z")).toEqual(true)
expect(validateDatetime("2015-12-31t23:59:60Z")).toEqual(true)
expect(validateDatetime("2015-02-11t22:59:22Z")).toEqual(true)
expect(validateDatetime("2020-01-01T20:00:00.000Z")).toEqual(true)
expect(validateDatetime("2020-01-01T20:00:00.000Z")).toEqual(true)
expect(validateDatetime("2023-05-04T01:14:00+21:00")).toEqual(true)
expect(validateDatetime("2023-05-04T01:14:10+16:20")).toEqual(true)
expect(validateDatetime("2023-05-04T01:14:21+09:50")).toEqual(true)
expect(validateDatetime("2023-05-04T01:14:21-04:31")).toEqual(true)
expect(validateDatetime("2023-05-04T01:14:21-23:59")).toEqual(true)
expect(validateDatetime("2016-15-31T23:59:60Z")).toEqual(false)
expect(validateDatetime("2015-00-11t22:59:22+00:00")).toEqual(false)
expect(validateDatetime("2015-01-00T22:59:22+00:00")).toEqual(false)
expect(validateDatetime("2016-12-31 23:59:60Z")).toEqual(false)
expect(validateDatetime("2015-02-11t24:59:22Z")).toEqual(false)
expect(validateDatetime("2020-01-01 20:00:00.000")).toEqual(false)
expect(validateDatetime("2020-01-01 20:00:00.000Z")).toEqual(false)
expect(validateDatetime("2023-05-04\t01:14:00+21:00")).toEqual(false)
expect(validateDatetime("2023-05-04\r01:14:10+16:20")).toEqual(false)
expect(validateDatetime("2015-02-11t22:59:22+24:30")).toEqual(false)
expect(validateDatetime("2023-05-04\n01:14:21+09:50")).toEqual(false)
expect(validateDatetime("2023-05-04\n01:14:21-04:31")).toEqual(false)
expect(validateDatetime("2023-05-04t01:14:21-04:31:00")).toEqual(false)

const validateIsoDatetime = ajv.compile({format: "iso-date-time"})
expect(validateIsoDatetime("2016-12-31 23:59:60Z")).toEqual(true)
expect(validateIsoDatetime("2016-13-31 23:59:60Z")).toEqual(false)
})
})

Expand Down