Skip to content

fix(typescript): preserve top-level array types from JSON Schema#3031

Merged
schani merged 13 commits into
masterfrom
agent/fix-issue-2481
Jul 23, 2026
Merged

fix(typescript): preserve top-level array types from JSON Schema#3031
schani merged 13 commits into
masterfrom
agent/fix-issue-2481

Conversation

@schani

@schani schani commented Jul 20, 2026

Copy link
Copy Markdown
Member

The bug

For a JSON Schema whose top-level type is an array, TypeScript (and JavaScript/Flow, which share the renderer) silently dropped the array wrapper and the element's own title. Given:

{
  "title": "TextClassificationOutput",
  "type": "array",
  "items": {
    "type": "object",
    "title": "TextClassificationOutputElement",
    "properties": {
      "label": { "type": "string" },
      "score": { "type": "number" }
    },
    "required": ["label", "score"]
  }
}

quicktype generated:

export interface Schema {
    label: string;
    score: number;
    [property: string]: any;
}

— no array-ness, and the element's own title (TextClassificationOutputElement) discarded. --lang go on the same schema was correct (type Schema []TextClassificationOutputElement plus a separate struct), confirming this was TS/JS/Flow-specific. A top-level array of primitives ({"type": "array", "items": {"type": "string"}, "title": "SomeInput"}) produced no output at all with --just-types.

Root cause

JavaScriptRenderer.namedTypeToNameForTopLevel unconditionally collapses a top-level array to its single named element type via directlyReachableSingleNamedType. That collapse is the correct, intentional behavior for JSON-sample-derived top-level arrays (a list of sample instances of one type — there's no title to preserve, and quicktype conventionally treats a top-level JSON-sample array as "one interface, many instances"). But it was applied unconditionally, so it also fired for JSON-Schema-derived top-level arrays, where the schema explicitly declares "type": "array" and the item's own title is meaningful and must be preserved.

The fix

  • Added a schemaArrayProvenance type attribute (packages/quicktype-core/src/attributes/InferenceFlags.ts) that JSONSchemaInput sets to "explicit" for arrays declared in real input JSON Schema, and to "inferred" for arrays that came from sample-based inference.
  • JavaScriptRenderer.namedTypeToNameForTopLevel now only collapses the array when it is not marked "explicit", so schema-declared top-level arrays keep their type X = Y[] alias and the element keeps its own schema-given name.
  • TypeScriptFlowBaseRenderer.emitTypes now also emits the array-alias line (export type X = Y[];) for these explicit schema arrays, matching how primitive top levels are already emitted.
  • Because quicktype's own --lang schema output is used internally to validate generated code (the diffViaSchema fixture check converts a JSON sample to a schema and regenerates from it, asserting identical output to direct-from-JSON generation), the schema writer (JSONSchemaRenderer) now marks a qt-inferred-top-level property on schemas it emits for arrays that were themselves sample-inferred, so that round-tripping through quicktype's own schema output doesn't change a sample-array's collapsed shape into an explicit schema-array shape.

Test coverage

  • New JSON Schema fixtures: test/inputs/schema/top-level-array.schema (titled object array, the original repro) and test/inputs/schema/top-level-primitive-array.schema (primitive array), each with a matching .1.json sample — these run automatically for TypeScript/JavaScript/Flow fixture suites (not in any skipSchema list).
  • New unit test test/unit/typescript-top-level-array.test.ts covering: schema array of objects emits the alias + titled interface, schema array of primitives emits the alias, and JSON-sample arrays still collapse to a single interface (no regression to existing sample-based behavior).

Verification

  • npm run build passes.
  • npm run test:unit — 167 tests pass.
  • Repro command now produces correct output:
    node dist/index.js --lang typescript --src-lang schema schema.json --just-types
    
    export type Schema = TextClassificationOutputElement[];
    
    export interface TextClassificationOutputElement {
        label: string;
        score: number;
        [property: string]: unknown;
    }
    (matches Go's naming behavior on the same input, type Schema []TextClassificationOutputElement, modulo per-language convention).
  • Verified the primitive-array case, the Flow equivalent, JSON-sample collapsing (unchanged), and the pre-existing union.schema fixture (top-level array of a union) all produce correct output.
  • QUICKTEST=true FIXTURE=typescript script/test — 78/78 pass (confirmed clean twice; one earlier run showed 2 failures that did not reproduce standalone and disappeared on rerun once I stopped running another fixture suite concurrently — resource contention in the parallel test harness, not a code issue).
  • QUICKTEST=true FIXTURE=javascript script/test — 57/57 pass.
  • Flow verified manually (the flow toolchain isn't installed in this environment, so its fixture suite wasn't runnable locally; CI will validate it).
  • Full CI fixture/schema matrix across all languages is left to CI.

Fixes #2481

🤖 Generated with Claude Code

schani and others added 5 commits July 20, 2026 18:06
Co-Authored-By: gpt-5.6-sol via pi <noreply@openai.com>
…vel-primitive-array.schema (#3031)

Co-Authored-By: Claude <noreply@anthropic.com>
Resolve conflict in JSONSchemaInput.ts makeArrayType: master added an
arrayAttributes parameter (from minMaxItems forArray producers) while this
branch adds schemaArrayTypeAttributeKind provenance. Combine both via
combineTypeAttributes, and keep the branch's makeTypeAttributesInferred
wrapping of singularAttributes.

Co-Authored-By: Claude <noreply@anthropic.com>
The new top-level-array.schema and top-level-primitive-array.schema
fixtures are top-level arrays, which neither the kotlinx renderer nor
the php driver can handle -- the same reason both already skip
union.schema. kotlinx emits `typealias TopLevel = JsonArray<T>`, which
does not compile (JsonArray takes no type arguments), and the php driver
does not support top-level arrays. Add both new schemas to those two
skipSchema lists.

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown

Generated-output differences

57 files differ — 3 modified, 54 new, 0 deleted
3675 changed lines — +3660 / −15

Open the generated-output report →

@github-actions

Copy link
Copy Markdown

Generated-output differences

63 files differ — 5 modified, 58 new, 0 deleted
3813 changed lines — +3798 / −15

Open the generated-output report →

@schani
schani merged commit 7d9a8a7 into master Jul 23, 2026
30 checks passed
@schani
schani deleted the agent/fix-issue-2481 branch July 23, 2026 19:45

// Preserve sample-inference semantics when a top-level array is round-tripped
// through quicktype's JSON Schema output.
export const inferredTopLevelSchemaComment = "qt-inferred-top-level";

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is stupid. Remove this, and fix the fallout properly!

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.

[Typescript] Top-level array don't get generated

1 participant