JTD compileParser rejects valid JSON encodings of enum values (raw byte compare against the JSON.stringify form)
(
.github/ISSUE_TEMPLATE/bug-or-error-report.md, labelbug report). Correctness /
enum escape,parser unicode. No matching reports found (re-checked 2026-07-20; #2595 is a different JTD parser bug, whitespace in empty arrays).
Title: JTD compileParser rejects valid JSON encodings of enum values (raw byte compare against the JSON.stringify form)
What version of Ajv are you using? Does the issue happen if you use the latest version?
8.20.0, reproduced on HEAD f177fe32. parseEnum is unchanged on master.
Ajv options object
// defaults; JTD entrypoint
const Ajv = require("ajv/dist/jtd").default
const ajv = new Ajv()JSON Schema (JTD)
{"enum": ["Active"]}Sample data
A valid JSON encoding of the allowed value "Active", spelling the A as a \\\u escape
(the input is the 13-character string below, backslash included):
"\u0041ctive"Your code
const parse = ajv.compileParser(schema)
const input = '"\\u0041ctive"'
console.log("ajv :", parse(input), parse.message)
console.log("oracle:", JSON.parse(input)) // any conformant JSON parserValidation result, data AFTER validation, error messages
ajv : undefined (parse.message = 'unexpected token \', position 1)
oracle: "Active""\u0041ctive" is a valid JSON encoding of "Active", an allowed enum value, yet the JTD
parser rejects it. The same holds for the escaped solidus and for the parser's own serializer
output:
enum ["Active"], input "\u0041ctive" -> ajv: undefined ('unexpected token \') JSON.parse: "Active"
enum ["A/B"], input "A\/B" -> ajv: undefined ('unexpected token A') JSON.parse: "A/B"
enum ["Active"], input "Active" -> ajv: "Active" (agree)The bug breaks ajv's own serialize/parse round-trip. compileSerializer escapes characters
through quote() (lib/runtime/quote.ts), which \\\u-escapes a superset of what
JSON.stringify escapes (DEL, C1 controls, U+00AD, U+200C-200F, U+2028-202F,
U+2060-206F, U+FEFF, ...). compileParser then rejects that output. Testing one enum value
per escaped character: 16 of 16 fail to round-trip, e.g.
enum ["x<ZWJ>y"] (ZWJ U+200D):
serializer output "x\u200dy" (valid JSON, JSON.parse recovers the value)
parser(serializer(v)) = undefined ('unexpected token x')Only the enum path is affected. Every other JTD string path decodes escapes, verified with the
same \u0041 escape:
{type: "string"} -> "Active"
{optionalProperties: {Active: ...}} -> {"Active": true} (escaped key matched)
{values: {type: "string"}} -> {"k": "Active"}
{enum: ["Active"]} -> undefined (only this path rejects)What results did you expect?
RFC 8927 defines enum membership on the decoded string value, so a JTD parser should accept
any valid JSON encoding of an allowed value. parse('"\\u0041ctive"') for {enum: ["Active"]}
should return "Active", matching JSON.parse, and the parser should accept everything the
serializer emits.
Root cause
lib/compile/jtd/parse.ts, parseEnum (line 302). Instead of decoding the JSON string token,
the generated code compares raw input bytes against the one encoding JSON.stringify happens
to produce:
function parseEnum(cxt: ParseCxt): void {
const {gen, data, schema} = cxt
const enumSch = schema.enum
parseToken(cxt, '"')
// TODO loopEnum
gen.if(false)
for (const value of enumSch) {
const valueStr = JSON.stringify(value).slice(1) // remove starting quote
gen.elseIf(_`${jsonSlice(valueStr.length)} === ${valueStr}`)
gen.assign(data, str`${value}`)
gen.add(N.jsonPos, valueStr.length)
}
gen.else()
jsonSyntaxError(cxt)
gen.endIf()
}JSON.stringify emits minimal escaping. A conformant producer may emit any equivalent encoding
(A, \/, escaped control or format characters), and the byte-slice === treats those as
non-matches. The other string paths go through parseJsonString, which decodes escapes;
parseEnum skips it. The // TODO loopEnum comment already marks this code as provisional.
Suggested fix
Decode the token first, then compare decoded values: route enum through the same
parseJsonString machinery the other string paths use, then check membership of the resulting
JS string in schema.enum. That makes the comparison encoding-independent and restores the
serialize/parse round-trip. A regression test should cover \\\uXXXX, \/, and round-tripping
serializer output for the characters quote() escapes.
Are you going to resolve the issue?
I can help. The clean fix reuses the parseJsonString path for the token; flagging the design
choice (decode-then-compare vs the current byte-slice) for maintainer preference first.
Found via property-based & differential bug-hunting, part of an effort to scale PBT (DepTyCheck-based) testing across the OSS ecosystem.
If this is intended / by-design: I'm really sorry, please just close it — no need to flag or ban me. I'm trying to scale property-based testing across the whole ecosystem and my publishing agents may have gotten this one wrong. I read every issue and follow up on each.
Source: ajv-validator/ajv