From 1f6f26ce96bbe16860b7eb7ea3c7e4cc476e228d Mon Sep 17 00:00:00 2001 From: Jason Green Date: Sat, 17 Aug 2024 11:49:22 +0100 Subject: [PATCH] feat: add safeNumbers option --- docs/options.md | 8 ++++++++ lib/compile/jtd/serialize.ts | 22 ++++++++++++++------ lib/core.ts | 1 + spec/jtd-schema.spec.ts | 40 ++++++++++++++++++++++++++++-------- 4 files changed, 57 insertions(+), 14 deletions(-) diff --git a/docs/options.md b/docs/options.md index fdce7f571b..aedb5c1754 100644 --- a/docs/options.md +++ b/docs/options.md @@ -204,6 +204,14 @@ 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. ::: +### safeNumbers + +Defines how special case numbers, Infinity, -Infinity and NaN are handled. Use `safeNumbers: "null"` to serialize them to `null` which is correct behavior according to the JSON spec. If you wish to handle these values, however, and not lose the data you can use `safeNumbers: "string"` which will serialize them to strings. If this option is not set the values will be included as the original literal values. + +::: warning The default behavior can produce invalid JSON +If `safeNumbers` is left undefined, the serializer will produce invalid JSON when there are any special case numbers in the data. This is, however, the fastest mode and so should be used unless you expect to encounter special case numbers. +::: + ### 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 3a0d15fcd7..4943fe96b7 100644 --- a/lib/compile/jtd/serialize.ts +++ b/lib/compile/jtd/serialize.ts @@ -228,12 +228,22 @@ function serializeString({gen, data}: SerializeCxt): void { gen.add(N.json, _`${useFunc(gen, quote)}(${data})`) } -function serializeNumber({gen, data}: SerializeCxt): void { - gen.if( - _`${data} === Infinity || ${data} === -Infinity || Number.isNaN(${data})`, - () => gen.add(N.json, _`null`), - () => gen.add(N.json, _`"" + ${data}`) - ) +function serializeNumber({gen, data, self}: SerializeCxt): void { + if (self.opts.safeNumbers === "null") { + gen.if( + _`${data} === Infinity || ${data} === -Infinity || Number.isNaN(${data})`, + () => gen.add(N.json, _`null`), + () => gen.add(N.json, _`"" + ${data}`) + ) + } else if (self.opts.safeNumbers === "string") { + gen.if( + _`${data} === Infinity || ${data} === -Infinity || Number.isNaN(${data})`, + () => gen.add(N.json, str`"${data}"`), + () => gen.add(N.json, _`"" + ${data}`) + ) + } else { + gen.add(N.json, _`"" + ${data}`) + } } function serializeRef(cxt: SerializeCxt): void { diff --git a/lib/core.ts b/lib/core.ts index e41ca3e2aa..4fcb4b94b3 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 + safeNumbers?: "string" | "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 f3a400be19..5110615b3b 100644 --- a/spec/jtd-schema.spec.ts +++ b/spec/jtd-schema.spec.ts @@ -146,16 +146,40 @@ describe("JSON Type Definition", () => { } }) - describe("serialize special numeric values", () => { - const ajv = new _AjvJTD() + describe.only("serialize special numeric values to null", () => { + describe("to null", () => { + const ajv = new _AjvJTD({safeNumbers: "null"}) - it(`should serialize Infinity to null`, () => { - const serialize = ajv.compileSerializer({type: "float64"}) - assert.deepStrictEqual(JSON.parse(serialize(Infinity)), null) + it(`should serialize Infinity to null`, () => { + const serialize = ajv.compileSerializer({type: "float64"}) + assert.deepStrictEqual(JSON.parse(serialize(Infinity)), null) + }) + it(`should serialize -Infinity to null`, () => { + const serialize = ajv.compileSerializer({type: "float64"}) + assert.deepStrictEqual(JSON.parse(serialize(-Infinity)), null) + }) + it(`should serialize NaN to null`, () => { + const serialize = ajv.compileSerializer({type: "float64"}) + assert.deepStrictEqual(JSON.parse(serialize(NaN)), null) + }) }) - it(`should serialize NaN to null`, () => { - const serialize = ajv.compileSerializer({type: "float64"}) - assert.deepStrictEqual(JSON.parse(serialize(NaN)), null) + + describe("to string", () => { + const ajv = new _AjvJTD({safeNumbers: "string"}) + + it(`should serialize Infinity to string`, () => { + const serialize = ajv.compileSerializer({type: "float64"}) + console.log(serialize(Infinity)) + assert.deepStrictEqual(JSON.parse(serialize(Infinity)), "Infinity") + }) + it(`should serialize -Infinity to string`, () => { + const serialize = ajv.compileSerializer({type: "float64"}) + assert.deepStrictEqual(JSON.parse(serialize(-Infinity)), "-Infinity") + }) + it(`should serialize NaN to string`, () => { + const serialize = ajv.compileSerializer({type: "float64"}) + assert.deepStrictEqual(JSON.parse(serialize(NaN)), "NaN") + }) }) })