Skip to content

Commit

Permalink
Handle recursive types #324 (#383)
Browse files Browse the repository at this point in the history
  • Loading branch information
darkowic authored Nov 24, 2020
1 parent 234757c commit b69cd3c
Show file tree
Hide file tree
Showing 9 changed files with 103 additions and 40 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@
"run": "ts-node typescript-json-schema-cli.ts",
"build": "tsc -p .",
"lint": "tslint --project tsconfig.json -c tslint.json --exclude '**/*.d.ts'",
"style": "yarn prettier --write *.js *.ts test/*.ts"
"style": "yarn prettier --write *.js *.ts test/*.ts",
"dev": "tsc -w -p .",
"test:dev": "mocha -t 5000 --watch --require source-map-support/register dist/test"
}
}
21 changes: 1 addition & 20 deletions test/programs/namespace-deep-1/schema.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$ref": "#/definitions/RootNamespace.Def",
"definitions": {
"RootNamespace.Def": {
"properties": {
Expand Down Expand Up @@ -55,26 +56,6 @@
"type": "object"
}
},
"properties": {
"nest": {
"$ref": "#/definitions/RootNamespace.Def"
},
"prev": {
"$ref": "#/definitions/RootNamespace.Def"
},
"propA": {
"$ref": "#/definitions/RootNamespace.SubNamespace.HelperA"
},
"propB": {
"$ref": "#/definitions/RootNamespace.SubNamespace.HelperB"
}
},
"required": [
"nest",
"prev",
"propA",
"propB"
],
"type": "object"
}

13 changes: 1 addition & 12 deletions test/programs/namespace-deep-2/schema.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$ref": "#/definitions/RootNamespace.SubNamespace.HelperA",
"definitions": {
"RootNamespace.Def": {
"properties": {
Expand Down Expand Up @@ -55,18 +56,6 @@
"type": "object"
}
},
"properties": {
"propA": {
"type": "number"
},
"propB": {
"$ref": "#/definitions/RootNamespace.SubNamespace.HelperB"
}
},
"required": [
"propA",
"propB"
],
"type": "object"
}

1 change: 1 addition & 0 deletions test/programs/type-globalThis/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type Test = typeof globalThis;
4 changes: 4 additions & 0 deletions test/programs/type-globalThis/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object"
}
8 changes: 8 additions & 0 deletions test/programs/type-recursive/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* A recursive type
*/
export type TestChildren = TestChild | Array<TestChild | TestChildren>;

interface TestChild {
type: string;
}
28 changes: 28 additions & 0 deletions test/programs/type-recursive/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$ref": "#/definitions/TestChildren",
"definitions": {
"TestChild": {
"properties": {
"type": {
"type": "string"
}
},
"required": ["type"],
"type": "object"
},
"TestChildren": {
"anyOf": [
{
"$ref": "#/definitions/TestChild"
},
{
"items": {
"$ref": "#/definitions/TestChildren"
},
"type": "array"
}
]
}
}
}
8 changes: 8 additions & 0 deletions test/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,14 @@ describe("schema", () => {
assertSchema("object-numeric-index", "IndexInterface");
assertSchema("object-numeric-index-as-property", "Target", { required: false });
});

describe("recursive type", () => {
assertSchema("type-recursive", "TestChildren");
});

describe("typeof globalThis", () => {
assertSchema("type-globalThis", "Test");
});
});

describe("tsconfig.json", () => {
Expand Down
56 changes: 49 additions & 7 deletions typescript-json-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -888,6 +888,13 @@ export class JsonSchemaGenerator {

private getClassDefinition(clazzType: ts.Type, definition: Definition): Definition {
const node = clazzType.getSymbol()!.getDeclarations()![0];

// Example: typeof globalThis may not have any declaration
if (!node) {
definition.type = "object";
return definition;
}

if (this.args.typeOfKeyword && node.kind === ts.SyntaxKind.FunctionType) {
definition.typeof = "function";
return definition;
Expand Down Expand Up @@ -1048,6 +1055,8 @@ export class JsonSchemaGenerator {
return name;
}

private recursiveTypeRef = new Map();

private getTypeDefinition(
typ: ts.Type,
asRef = this.args.ref,
Expand Down Expand Up @@ -1084,9 +1093,11 @@ export class JsonSchemaGenerator {
// FIXME: We can't just compare the name of the symbol - it ignores the namespace
const isRawType =
!symbol ||
this.tc.getFullyQualifiedName(symbol) === "Date" ||
symbol.name === "integer" ||
this.tc.getIndexInfoOfType(typ, ts.IndexKind.Number) !== undefined;
// Window is incorrectly marked as rawType here for some reason
(this.tc.getFullyQualifiedName(symbol) !== "Window" &&
(this.tc.getFullyQualifiedName(symbol) === "Date" ||
symbol.name === "integer" ||
this.tc.getIndexInfoOfType(typ, ts.IndexKind.Number) !== undefined));

// special case: an union where all child are string literals -> make an enum instead
let isStringEnum = false;
Expand All @@ -1106,6 +1117,7 @@ export class JsonSchemaGenerator {
) {
asRef = false; // raw types and inline types cannot be reffed,
// unless we are handling a type alias
// or it is recursive type - see below
}
}

Expand All @@ -1116,15 +1128,16 @@ export class JsonSchemaGenerator {
reffedType!.getFlags() & ts.SymbolFlags.Alias ? this.tc.getAliasedSymbol(reffedType!) : reffedType!
)
.replace(REGEX_FILE_NAME_OR_SPACE, "");
if (this.args.uniqueNames) {
const sourceFile = getSourceFile(reffedType!);
if (this.args.uniqueNames && reffedType) {
const sourceFile = getSourceFile(reffedType);
const relativePath = path.relative(process.cwd(), sourceFile.fileName);
fullTypeName = `${typeName}.${generateHashOfNode(getCanonicalDeclaration(reffedType!), relativePath)}`;
} else {
fullTypeName = this.makeTypeNameUnique(typ, typeName);
}
} else if (asRef) {
if (this.args.uniqueNames) {
} else {
// typ.symbol can be undefined
if (this.args.uniqueNames && typ.symbol) {
const sym = typ.symbol;
const sourceFile = getSourceFile(sym);
const relativePath = path.relative(process.cwd(), sourceFile.fileName);
Expand All @@ -1139,6 +1152,15 @@ export class JsonSchemaGenerator {
}
}

// Handle recursive types
if (!isRawType || !!typ.aliasSymbol) {
if (this.recursiveTypeRef.has(fullTypeName)) {
asRef = true;
} else {
this.recursiveTypeRef.set(fullTypeName, definition);
}
}

if (asRef) {
// We don't return the full definition, but we put it into
// reffedDefinitions below.
Expand Down Expand Up @@ -1227,6 +1249,26 @@ export class JsonSchemaGenerator {
}
}

if (this.recursiveTypeRef.get(fullTypeName) === definition) {
this.recursiveTypeRef.delete(fullTypeName);
// If the type was recursive (there is reffedDefinitions) - lets replace it to reference
if (this.reffedDefinitions[fullTypeName]) {
// Here we may want to filter out all type specific fields
// and include fields like description etc
const annotations = Object.entries(returnedDefinition).reduce((acc, [key, value]) => {
if (validationKeywords[key] && typeof value !== undefined) {
acc[key] = value;
}
return acc;
}, {});

returnedDefinition = {
$ref: `${this.args.id}#/definitions/` + fullTypeName,
...annotations,
};
}
}

if (otherAnnotations["nullable"]) {
makeNullable(returnedDefinition);
}
Expand Down

0 comments on commit b69cd3c

Please sign in to comment.