diff --git a/docs/options.md b/docs/options.md index fdce7f571b..e8465e8152 100644 --- a/docs/options.md +++ b/docs/options.md @@ -204,6 +204,19 @@ Defines how date-time strings are parsed and validated. By default Ajv only allo 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. ::: +### specialNumbers + +Defines how special case numbers `Infinity`, `-Infinity` and `NaN` are handled. + +Option values: + +- `"fast"` - (default): Do not treat special numbers differently to normal numbers. This is the fastest method but also can produce invalid JSON if the data contains special numbers. +- `"null"` - Special numbers will be serialized to `null` which is the correct behavior according to the JSON spec and is also the same behavior as `JSON.stringify`. + +::: warning The default behavior can produce invalid JSON +Using `specialNumbers: "fast" or undefined` can produce invalid JSON when there are any special case numbers in the data. +::: + ### int32range Can be used to disable range checking for `int32` and `uint32` types. diff --git a/lib/compile/jtd/serialize.ts b/lib/compile/jtd/serialize.ts index 1d228826d4..42a47cffc1 100644 --- a/lib/compile/jtd/serialize.ts +++ b/lib/compile/jtd/serialize.ts @@ -228,8 +228,19 @@ function serializeString({gen, data}: SerializeCxt): void { gen.add(N.json, _`${useFunc(gen, quote)}(${data})`) } -function serializeNumber({gen, data}: SerializeCxt): void { - gen.add(N.json, _`"" + ${data}`) +function serializeNumber({gen, data, self}: SerializeCxt): void { + const condition = _`${data} === Infinity || ${data} === -Infinity || ${data} !== ${data}` + + if (self.opts.specialNumbers === undefined || self.opts.specialNumbers === "fast") { + gen.add(N.json, _`"" + ${data}`) + } else { + // specialNumbers === "null" + gen.if( + condition, + () => gen.add(N.json, _`null`), + () => gen.add(N.json, _`"" + ${data}`) + ) + } } function serializeRef(cxt: SerializeCxt): void { diff --git a/lib/core.ts b/lib/core.ts index e41ca3e2aa..6ceedf5419 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -107,6 +107,7 @@ export interface CurrentOptions { timestamp?: "string" | "date" // JTD only parseDate?: boolean // JTD only allowDate?: boolean // JTD only + specialNumbers?: "fast" | "null" // JTD only $comment?: | true | ((comment: string, schemaPath?: string, rootSchema?: AnySchemaObject) => unknown) diff --git a/spec/jtd-schema.spec.ts b/spec/jtd-schema.spec.ts index f4881b18a4..b9cf3ab695 100644 --- a/spec/jtd-schema.spec.ts +++ b/spec/jtd-schema.spec.ts @@ -146,6 +146,53 @@ describe("JSON Type Definition", () => { } }) + describe("serialize special numeric values", () => { + describe("fast", () => { + const ajv = new _AjvJTD({specialNumbers: "fast"}) + + it(`should serialize Infinity to literal`, () => { + const serialize = ajv.compileSerializer({type: "float64"}) + const res = serialize(Infinity) + assert.equal(res, "Infinity") + assert.throws(() => JSON.parse(res)) + }) + it(`should serialize -Infinity to literal`, () => { + const serialize = ajv.compileSerializer({type: "float64"}) + const res = serialize(-Infinity) + assert.equal(res, "-Infinity") + assert.throws(() => JSON.parse(res)) + }) + it(`should serialize NaN to literal`, () => { + const serialize = ajv.compileSerializer({type: "float64"}) + const res = serialize(NaN) + assert.equal(res, "NaN") + assert.throws(() => JSON.parse(res)) + }) + }) + describe("to null", () => { + const ajv = new _AjvJTD({specialNumbers: "null"}) + + it(`should serialize Infinity to null`, () => { + const serialize = ajv.compileSerializer({type: "float64"}) + const res = serialize(Infinity) + assert.equal(res, "null") + assert.equal(JSON.parse(res), null) + }) + it(`should serialize -Infinity to null`, () => { + const serialize = ajv.compileSerializer({type: "float64"}) + const res = serialize(-Infinity) + assert.equal(res, "null") + assert.equal(JSON.parse(res), null) + }) + it(`should serialize NaN to null`, () => { + const serialize = ajv.compileSerializer({type: "float64"}) + const res = serialize(NaN) + assert.equal(res, "null") + assert.equal(JSON.parse(res), null) + }) + }) + }) + describe("parse", () => { let ajv: AjvJTD before(() => (ajv = new _AjvJTD()))