Skip to content

Commit

Permalink
fix: #2482 Infinity and NaN serialise to null (#2487)
Browse files Browse the repository at this point in the history
* fix: #2482 Infinity and NaN serialise to null

* feat: add safeNumbers option

* include test for default behavior

* change option name to specialNumbers

* refactor to be more dry

* pr feedback

* remove string option
  • Loading branch information
jasoniangreen authored Sep 15, 2024
1 parent f06766f commit 69568d0
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 2 deletions.
13 changes: 13 additions & 0 deletions docs/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Badge text="JTD only" />

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 <Badge text="JTD only" />

Can be used to disable range checking for `int32` and `uint32` types.
Expand Down
15 changes: 13 additions & 2 deletions lib/compile/jtd/serialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions lib/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
47 changes: 47 additions & 0 deletions spec/jtd-schema.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
Expand Down

0 comments on commit 69568d0

Please sign in to comment.