Skip to content

fix(schema-input): decode percent-encoded JSON Pointer segments in $ref - #2995

Open
schani wants to merge 5 commits into
masterfrom
agent/fix-issue-1992
Open

fix(schema-input): decode percent-encoded JSON Pointer segments in $ref#2995
schani wants to merge 5 commits into
masterfrom
agent/fix-issue-1992

Conversation

@schani

@schani schani commented Jul 20, 2026

Copy link
Copy Markdown
Member

The bug

quicktype -s typescript -l c++ ... (and -l typescript) fatally errors on
generic classes instantiated with more than one type argument, e.g.:

export class StateInfo<T> {
    public state: T;
    public changeTime: number;
}
export enum xxx { jam = "xx", jest = "yy" }
export enum www { Disabled = "asd", Ok = "qwe" }
export class foo { public state: StateInfo<xxx>; }
export class woo { public test2: StateInfo<www>; }
Error: Key StateInfo%3CT%3E not in schema object at /#StateInfo%3CT%3E.

No output files are produced at all. This is a regression from the original
report in #1992, where instead of erroring, quicktype silently misnamed the
first generic instantiation's enum (PurpleStateInfoT) — same root cause,
different (now worse) symptom.

Root cause

quicktype-typescript-input converts TypeScript generics into monomorphized
JSON Schema definitions, e.g. a definition literally named "StateInfo<T>",
referenced via "$ref": "#/definitions/StateInfo%3CT%3E" (URI-fragment
percent-encoded, since </> aren't valid in a bare JSON Pointer/URI
fragment).

quicktype-core's Ref.parsePath (in
packages/quicktype-core/src/input/JSONSchemaInput.ts), which turns a $ref
fragment into path segments for definition lookup, split the fragment on /
but never percent-decoded the segments. So it looked up the literal key
"StateInfo%3CT%3E" in schema.definitions, which doesn't exist (the real
key is "StateInfo<T>"), and failed with SchemaKeyNotInObject. Per RFC 6901
§6, a URI fragment used as a JSON Pointer must be percent-decoded before its
segments are interpreted — that step was missing.

Separately, schemaForTypeScriptSources in
packages/quicktype-typescript-input/src/index.ts built top-level $ref
URIs without percent-encoding special characters in the name, and its
fallback for selecting "top-level" types (when no #TopLevel marker is
present) picked up internal helper definitions rather than the program's
actual exported symbols — this fallback path is what produced the original
PurpleStateInfoT misnaming.

The fix

  • Ref.parsePath: percent-decode each /-separated path segment after
    splitting (so StateInfo%3CT%3E resolves to the real StateInfo<T> key).
  • schemaForTypeScriptSources: percent-encode generated top-level definition
    URIs to stay consistent with how $refs are encoded elsewhere, and use
    generator.getMainFileSymbols() (the program's actual exported symbols) as
    the top-level type set instead of arbitrary internal definitions when no
    #TopLevel marker is present.

Test coverage

  • test/inputs/schema/percent-encoded-ref.schema (+ .1.json sample): a new
    JSON Schema fixture whose $refs use percent-encoded characters (<, >)
    in the JSON Pointer, directly exercising Ref.parsePath. Fails against
    unfixed code with the same SchemaKeyNotInObject error; passes after the
    fix. Runs automatically for every schema-<language> fixture (verified
    locally for schema-typescript and schema-cplusplus).
  • test/unit/typescript-input.test.ts: new regression test
    ("generic instantiations retain their exported type names") reproducing
    the exact ts to C++ class template argument renamed #1992 repro end-to-end through quicktype() with the
    TypeScript-source input, asserting the two monomorphized classes get
    distinct correct names (StateInfoXxx, StateInfoWWW) and that neither
    PurpleStateInfoT nor StateInfoT1 appear.

Verification performed locally

  • npm run build passes.
  • npm run test:unit: 164/164 tests pass (25 files).
  • QUICKTEST=true FIXTURE=schema-typescript script/test test/inputs/schema/percent-encoded-ref.schema passes.
  • QUICKTEST=true FIXTURE=schema-cplusplus script/test test/inputs/schema/percent-encoded-ref.schema passes (compiled and ran with g++/nlohmann-json).
  • Re-ran the exact issue repro (-s typescript -l c++ ... with the sample
    from ts to C++ class template argument renamed #1992, both in original and reversed class-declaration order):
    previously fatal, now generates StateInfoXxx.hpp / StateInfoWww.hpp
    (and the TypeScript-language equivalent) with correct, order-independent
    names and no PurpleStateInfoT/collision renaming.
  • Confirmed an unrelated, pre-existing failure in the full schema-typescript
    fixture run (test/inputs/schema/vega-lite.schema fails to compile with
    tsc due to an unrelated index-signature type conflict) reproduces
    identically on unmodified master, so it is not caused by this change; CI
    will cover the rest of the fixture matrix.

Fixes #1992.

🤖 Generated with Claude Code

schani and others added 5 commits July 20, 2026 17:11
…ef (#1992)

Co-Authored-By: gpt-5.6-sol via pi <noreply@openai.com>
…r non-validating langs

The getMainFileSymbols fallback in schemaForTypeScriptSources (added for
#1992) left the source name empty when it produced a single top-level ref.
For a single schema source the JSON Schema input uses that empty name
verbatim, so the top-level type was rendered as the synthesized "Empty"
instead of its exported name. This broke the json-ts-csharp fixture: the
generated C# no longer contained a TopLevel class, and the driver failed to
compile with CS0103. When exactly one exported symbol is selected, name it
explicitly so the round-trip type keeps its name.

Also add percent-encoded-ref.schema to skipsEnumValueValidation: its only
negative sample rejects an out-of-range enum value, which languages that do
not validate enum values (cjson, crystal, swift, haskell, typescript-zod,
typescript-effect-schema) cannot fail, so the expected-failure case was
passing unexpectedly.

Co-Authored-By: Claude <noreply@anthropic.com>
# Conflicts:
#	test/languages.ts
#	test/unit/typescript-input.test.ts
@github-actions

Copy link
Copy Markdown

Generated-output differences

40 files differ — 0 modified, 40 new, 0 deleted
3998 changed lines — +3998 / −0

Open the generated-output report →

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.

ts to C++ class template argument renamed

2 participants