Skip to content

Commit 69568d0

Browse files
fix: #2482 Infinity and NaN serialise to null (#2487)
* 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
1 parent f06766f commit 69568d0

File tree

4 files changed

+74
-2
lines changed

4 files changed

+74
-2
lines changed

docs/options.md

+13
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,19 @@ Defines how date-time strings are parsed and validated. By default Ajv only allo
204204
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.
205205
:::
206206

207+
### specialNumbers <Badge text="JTD only" />
208+
209+
Defines how special case numbers `Infinity`, `-Infinity` and `NaN` are handled.
210+
211+
Option values:
212+
213+
- `"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.
214+
- `"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`.
215+
216+
::: warning The default behavior can produce invalid JSON
217+
Using `specialNumbers: "fast" or undefined` can produce invalid JSON when there are any special case numbers in the data.
218+
:::
219+
207220
### int32range <Badge text="JTD only" />
208221

209222
Can be used to disable range checking for `int32` and `uint32` types.

lib/compile/jtd/serialize.ts

+13-2
Original file line numberDiff line numberDiff line change
@@ -228,8 +228,19 @@ function serializeString({gen, data}: SerializeCxt): void {
228228
gen.add(N.json, _`${useFunc(gen, quote)}(${data})`)
229229
}
230230

231-
function serializeNumber({gen, data}: SerializeCxt): void {
232-
gen.add(N.json, _`"" + ${data}`)
231+
function serializeNumber({gen, data, self}: SerializeCxt): void {
232+
const condition = _`${data} === Infinity || ${data} === -Infinity || ${data} !== ${data}`
233+
234+
if (self.opts.specialNumbers === undefined || self.opts.specialNumbers === "fast") {
235+
gen.add(N.json, _`"" + ${data}`)
236+
} else {
237+
// specialNumbers === "null"
238+
gen.if(
239+
condition,
240+
() => gen.add(N.json, _`null`),
241+
() => gen.add(N.json, _`"" + ${data}`)
242+
)
243+
}
233244
}
234245

235246
function serializeRef(cxt: SerializeCxt): void {

lib/core.ts

+1
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ export interface CurrentOptions {
107107
timestamp?: "string" | "date" // JTD only
108108
parseDate?: boolean // JTD only
109109
allowDate?: boolean // JTD only
110+
specialNumbers?: "fast" | "null" // JTD only
110111
$comment?:
111112
| true
112113
| ((comment: string, schemaPath?: string, rootSchema?: AnySchemaObject) => unknown)

spec/jtd-schema.spec.ts

+47
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,53 @@ describe("JSON Type Definition", () => {
146146
}
147147
})
148148

149+
describe("serialize special numeric values", () => {
150+
describe("fast", () => {
151+
const ajv = new _AjvJTD({specialNumbers: "fast"})
152+
153+
it(`should serialize Infinity to literal`, () => {
154+
const serialize = ajv.compileSerializer({type: "float64"})
155+
const res = serialize(Infinity)
156+
assert.equal(res, "Infinity")
157+
assert.throws(() => JSON.parse(res))
158+
})
159+
it(`should serialize -Infinity to literal`, () => {
160+
const serialize = ajv.compileSerializer({type: "float64"})
161+
const res = serialize(-Infinity)
162+
assert.equal(res, "-Infinity")
163+
assert.throws(() => JSON.parse(res))
164+
})
165+
it(`should serialize NaN to literal`, () => {
166+
const serialize = ajv.compileSerializer({type: "float64"})
167+
const res = serialize(NaN)
168+
assert.equal(res, "NaN")
169+
assert.throws(() => JSON.parse(res))
170+
})
171+
})
172+
describe("to null", () => {
173+
const ajv = new _AjvJTD({specialNumbers: "null"})
174+
175+
it(`should serialize Infinity to null`, () => {
176+
const serialize = ajv.compileSerializer({type: "float64"})
177+
const res = serialize(Infinity)
178+
assert.equal(res, "null")
179+
assert.equal(JSON.parse(res), null)
180+
})
181+
it(`should serialize -Infinity to null`, () => {
182+
const serialize = ajv.compileSerializer({type: "float64"})
183+
const res = serialize(-Infinity)
184+
assert.equal(res, "null")
185+
assert.equal(JSON.parse(res), null)
186+
})
187+
it(`should serialize NaN to null`, () => {
188+
const serialize = ajv.compileSerializer({type: "float64"})
189+
const res = serialize(NaN)
190+
assert.equal(res, "null")
191+
assert.equal(JSON.parse(res), null)
192+
})
193+
})
194+
})
195+
149196
describe("parse", () => {
150197
let ajv: AjvJTD
151198
before(() => (ajv = new _AjvJTD()))

0 commit comments

Comments
 (0)