Skip to content

fix: serialize const string values to valid JSON#848

Open
spokodev wants to merge 1 commit into
fastify:mainfrom
spokodev:fix/const-string-escaping
Open

fix: serialize const string values to valid JSON#848
spokodev wants to merge 1 commit into
fastify:mainfrom
spokodev:fix/const-string-escaping

Conversation

@spokodev

Copy link
Copy Markdown

Problem

A const string containing a backslash, control character, or double quote is serialized into corrupted or invalid JSON, even when the input matches the schema:

const build = require('fast-json-stringify')

build({ type: 'object', properties: { v: { const: 'a\\b' } } })({ v: 'a\\b' })
// -> {"v":"a\b"}            JSON.parse -> "a\b" (the value becomes a backspace)

build({ type: 'object', properties: { v: { const: 'a\tb' } } })({ v: 'a\tb' })
// -> {"v":"a<RAW TAB>b"}    JSON.parse -> SyntaxError: Bad control character

build({ type: 'object', properties: { v: { const: 'q"q' } } })({ v: 'q"q' })
// -> {"v":"q"q"}            JSON.parse -> SyntaxError

This reproduces for const at the top level, in properties, in array items, with type set, and as a nullable. enum and default with the same values serialize correctly — only const is affected.

Cause

JSON.stringify(schema.const) produces correct JSON text, but it is then embedded into a single-quoted JS string literal in the generated function source, escaping only single quotes (SINGLE_TICK):

code += `json += '${JSON.stringify(schema.const).replace(SINGLE_TICK, "\\'")}'`

When the Function constructor evaluates that source, \\ collapses back to \, a \t becomes a raw tab, and an embedded " breaks the JSON string. (Introduced by #658, which handled only the single-quote case.)

Fix

JSON.stringify the JSON text a second time to produce a properly escaped JS string literal — the same approach already used for default values:

code += `json += ${JSON.stringify(JSON.stringify(schema.const))}`

SINGLE_TICK is now unused and removed. This also still covers the original single-quote case from #658.

Tests

Added a case to test/const.test.js over ['back\\slash', 'tab\there', 'quote"here', 'new\nline', "tick's"] asserting the output equals JSON.stringify({ foo: value }) and round-trips (red before, green after).

Full unit suite green (473/473). Note: I ran it via node --test directly, because c8's bundled yargs cannot load under Node 26 (require is not defined in ES module scope) — unrelated to this change. npm run test:typescript passes.

A `const` string containing a backslash, control character, or double
quote was serialized into corrupted or invalid JSON, even though the input
matched the schema:

    build({ properties: { v: { const: 'a\\b' } } })({ v: 'a\\b' })
    // -> {"v":"a\b"}  -> JSON.parse yields "a\b" (a backspace)

    build({ properties: { v: { const: 'a\tb' } } })({ v: 'a\tb' })
    // -> {"v":"a<TAB>b"} -> JSON.parse throws "Bad control character"

The value of `JSON.stringify(schema.const)` was embedded into a
single-quoted string literal in the generated function source while only
single quotes were escaped, so the `Function` constructor then re-unescaped
backslashes and control characters. `JSON.stringify` the JSON text once more
to produce a properly escaped JS string literal, mirroring how default
values are already emitted. `SINGLE_TICK` is now unused and removed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant