Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion packages/quicktype-core/src/input/JSONSchemaInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,9 +197,11 @@ export class Ref {

public addressURI: URI | undefined;

public readonly path: readonly PathElement[];

public constructor(
addressURI: URI | undefined,
public readonly path: readonly PathElement[],
path: readonly PathElement[],
) {
if (addressURI !== undefined) {
assert(
Expand All @@ -210,6 +212,13 @@ export class Ref {
} else {
this.addressURI = undefined;
}

// An addressed document root must match an explicit `#/` reference so
// paths appended to either form remain canonical and comparable.
this.path =
addressURI !== undefined && path.length === 0
? [{ kind: PathElementKind.Root }]
: path;
}

public get hasAddress(): boolean {
Expand Down
4 changes: 4 additions & 0 deletions test/inputs/schema/recursive-ref-to-id.1.fail.union.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"data": { "id": "root" },
"children": "not-an-array"
}
11 changes: 11 additions & 0 deletions test/inputs/schema/recursive-ref-to-id.1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"data": { "id": "root" },
"children": [
{
"data": { "id": "child" },
"children": [
{ "data": { "id": "grandchild" } }
]
}
]
}
17 changes: 17 additions & 0 deletions test/inputs/schema/recursive-ref-to-id.schema
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "T2",
"type": "object",
"properties": {
"data": {
"type": "object",
"properties": {
"id": { "type": "string" }
}
},
"children": {
"type": "array",
"items": { "$ref": "T2" }
}
}
}
4 changes: 4 additions & 0 deletions test/languages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,7 @@ export const CJSONLanguage: Language = {
"multi-type-enum.schema",
"nested-intersection-union.schema",
"prefix-items.schema",
"recursive-ref-to-id.schema",
/* Constraints (min/max and regex) are not supported (for the current implementation, can be added later, should abord parsing and return NULL) */
"minmaxlength.schema",
"schema-constraints.schema",
Expand Down Expand Up @@ -923,6 +924,7 @@ export const ElmLanguage: Language = {
"integer-before-number.schema", // Python-specific union-order regression.
"union-list.schema", // recursion
"list.schema", // recursion
"recursive-ref-to-id.schema", // recursion
"ref-remote.schema", // recursion
"mutually-recursive.schema", // recursion
"postman-collection.schema", // recursion
Expand Down Expand Up @@ -1759,6 +1761,7 @@ export const DartLanguage: Language = {
"mutually-recursive.schema",
"postman-collection.schema",
"list.schema",
"recursive-ref-to-id.schema",
"simple-ref.schema",
"keyword-unions.schema",
"ref-remote.schema",
Expand Down Expand Up @@ -1911,6 +1914,7 @@ export const HaskellLanguage: Language = {
"nested-intersection-union.schema",
"prefix-items.schema",
"direct-union.schema",
"recursive-ref-to-id.schema",
...skipsEnumValueValidation,
...skipsMapValueValidation,
"intersection.schema",
Expand Down
42 changes: 42 additions & 0 deletions test/unit/recursive-ref-to-id.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import * as fs from "node:fs";

import {
FetchingJSONSchemaStore,
InputData,
JSONSchemaInput,
quicktype,
} from "quicktype-core";
import { describe, expect, test } from "vitest";

const schema = fs.readFileSync(
"test/inputs/schema/recursive-ref-to-id.schema",
"utf8",
);

async function generateTypeScript(schemaText = schema): Promise<string> {
const schemaInput = new JSONSchemaInput(new FetchingJSONSchemaStore());
await schemaInput.addSource({ name: "Tree", schema: schemaText });
const inputData = new InputData();
inputData.addInput(schemaInput);

const result = await quicktype({ inputData, lang: "typescript" });
return result.lines.join("\n");
}

describe("recursive JSON Schema refs to $id (issue #2836)", () => {
test("reuses the top-level type for a self-reference", async () => {
const output = await generateTypeScript();

expect(output.match(/^export interface /gm)).toHaveLength(2);
expect(output).toMatch(/children\?:\s+Tree\[\];/);
});

test("reuses the top-level type for an empty-fragment self-reference", async () => {
const output = await generateTypeScript(
schema.replace('"$ref": "T2"', '"$ref": "#"'),
);

expect(output.match(/^export interface /gm)).toHaveLength(2);
expect(output).toMatch(/children\?:\s+Tree\[\];/);
});
});
8 changes: 5 additions & 3 deletions test/unit/rust-cycle-breaker-boxing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,15 @@ describe("Rust cycle-breaker boxing", () => {
anyOf: [...recursiveUnionMembers, { type: "null" }],
});

expect(output).toContain("pub next: Option<Box<Next>>,");
expect(output).not.toContain("Box<Option<Box<Next>>>");
expect(output).toContain("Node(Box<Node>),");
expect(output).toContain("pub next: Option<Next>,");
expect(output).not.toContain("Box<Option<Next>>");
});

test("keeps a non-nullable cycle-breaking union boxed", async () => {
const output = await renderRust({ anyOf: recursiveUnionMembers }, true);

expect(output).toContain("pub next: Box<Next>,");
expect(output).toContain("Node(Box<Node>),");
expect(output).toContain("pub next: Next,");
});
});
39 changes: 39 additions & 0 deletions test/unit/schema-properties-as-types.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {
FetchingJSONSchemaStore,
InputData,
JSONSchemaInput,
quicktype,
} from "quicktype-core";
import { describe, expect, test } from "vitest";

async function generateTypeScript(name: string, uri: string): Promise<string> {
const schemaInput = new JSONSchemaInput(new FetchingJSONSchemaStore());
await schemaInput.addSource({ name, uris: [uri] });
const inputData = new InputData();
inputData.addInput(schemaInput);

const result = await quicktype({ inputData, lang: "typescript" });
return result.lines.join("\n");
}

describe("schema URI properties-as-types mode", () => {
test("a trailing slash at the document root keeps the document as one type", async () => {
const output = await generateTypeScript(
"Test",
"test/inputs/schema/a/test2.json#/",
);

expect(output).toContain("export interface Test");
expect(output).toMatch(/foo:\s+number/);
});

test("a trailing slash below the root makes properties separate types", async () => {
const output = await generateTypeScript(
"Definitions",
"test/inputs/schema/b/test3.json#/definitions/",
);

expect(output).toContain("export interface Foo");
expect(output).toMatch(/foo:\s+number/);
});
});
Loading