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()))