#2651·ajv

JTD compileParser rejects valid JSON encodings of enum values (raw byte compare against the JSON.stringify form)

Author: haskiindahouseCreated Jul 21, 2026Updated Jul 21, 2026

(.github/ISSUE_TEMPLATE/bug-or-error-report.md, label bug 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

javascript
// defaults; JTD entrypoint
const Ajv = require("ajv/dist/jtd").default
const ajv = new Ajv()

JSON Schema (JTD)

json
{"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

javascript
const parse = ajv.compileParser(schema)
const input = '"\\u0041ctive"'
console.log("ajv   :", parse(input), parse.message)
console.log("oracle:", JSON.parse(input)) // any conformant JSON parser

Validation 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:

javascript
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.