Skip to content

Commit b69cd3c

Browse files
authored
Handle recursive types #324 (#383)
1 parent 234757c commit b69cd3c

File tree

9 files changed

+103
-40
lines changed

9 files changed

+103
-40
lines changed

package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@
7373
"run": "ts-node typescript-json-schema-cli.ts",
7474
"build": "tsc -p .",
7575
"lint": "tslint --project tsconfig.json -c tslint.json --exclude '**/*.d.ts'",
76-
"style": "yarn prettier --write *.js *.ts test/*.ts"
76+
"style": "yarn prettier --write *.js *.ts test/*.ts",
77+
"dev": "tsc -w -p .",
78+
"test:dev": "mocha -t 5000 --watch --require source-map-support/register dist/test"
7779
}
7880
}

test/programs/namespace-deep-1/schema.json

+1-20
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"$ref": "#/definitions/RootNamespace.Def",
34
"definitions": {
45
"RootNamespace.Def": {
56
"properties": {
@@ -55,26 +56,6 @@
5556
"type": "object"
5657
}
5758
},
58-
"properties": {
59-
"nest": {
60-
"$ref": "#/definitions/RootNamespace.Def"
61-
},
62-
"prev": {
63-
"$ref": "#/definitions/RootNamespace.Def"
64-
},
65-
"propA": {
66-
"$ref": "#/definitions/RootNamespace.SubNamespace.HelperA"
67-
},
68-
"propB": {
69-
"$ref": "#/definitions/RootNamespace.SubNamespace.HelperB"
70-
}
71-
},
72-
"required": [
73-
"nest",
74-
"prev",
75-
"propA",
76-
"propB"
77-
],
7859
"type": "object"
7960
}
8061

test/programs/namespace-deep-2/schema.json

+1-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"$ref": "#/definitions/RootNamespace.SubNamespace.HelperA",
34
"definitions": {
45
"RootNamespace.Def": {
56
"properties": {
@@ -55,18 +56,6 @@
5556
"type": "object"
5657
}
5758
},
58-
"properties": {
59-
"propA": {
60-
"type": "number"
61-
},
62-
"propB": {
63-
"$ref": "#/definitions/RootNamespace.SubNamespace.HelperB"
64-
}
65-
},
66-
"required": [
67-
"propA",
68-
"propB"
69-
],
7059
"type": "object"
7160
}
7261

test/programs/type-globalThis/main.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export type Test = typeof globalThis;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"type": "object"
4+
}

test/programs/type-recursive/main.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* A recursive type
3+
*/
4+
export type TestChildren = TestChild | Array<TestChild | TestChildren>;
5+
6+
interface TestChild {
7+
type: string;
8+
}
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"$ref": "#/definitions/TestChildren",
4+
"definitions": {
5+
"TestChild": {
6+
"properties": {
7+
"type": {
8+
"type": "string"
9+
}
10+
},
11+
"required": ["type"],
12+
"type": "object"
13+
},
14+
"TestChildren": {
15+
"anyOf": [
16+
{
17+
"$ref": "#/definitions/TestChild"
18+
},
19+
{
20+
"items": {
21+
"$ref": "#/definitions/TestChildren"
22+
},
23+
"type": "array"
24+
}
25+
]
26+
}
27+
}
28+
}

test/schema.test.ts

+8
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,14 @@ describe("schema", () => {
388388
assertSchema("object-numeric-index", "IndexInterface");
389389
assertSchema("object-numeric-index-as-property", "Target", { required: false });
390390
});
391+
392+
describe("recursive type", () => {
393+
assertSchema("type-recursive", "TestChildren");
394+
});
395+
396+
describe("typeof globalThis", () => {
397+
assertSchema("type-globalThis", "Test");
398+
});
391399
});
392400

393401
describe("tsconfig.json", () => {

typescript-json-schema.ts

+49-7
Original file line numberDiff line numberDiff line change
@@ -888,6 +888,13 @@ export class JsonSchemaGenerator {
888888

889889
private getClassDefinition(clazzType: ts.Type, definition: Definition): Definition {
890890
const node = clazzType.getSymbol()!.getDeclarations()![0];
891+
892+
// Example: typeof globalThis may not have any declaration
893+
if (!node) {
894+
definition.type = "object";
895+
return definition;
896+
}
897+
891898
if (this.args.typeOfKeyword && node.kind === ts.SyntaxKind.FunctionType) {
892899
definition.typeof = "function";
893900
return definition;
@@ -1048,6 +1055,8 @@ export class JsonSchemaGenerator {
10481055
return name;
10491056
}
10501057

1058+
private recursiveTypeRef = new Map();
1059+
10511060
private getTypeDefinition(
10521061
typ: ts.Type,
10531062
asRef = this.args.ref,
@@ -1084,9 +1093,11 @@ export class JsonSchemaGenerator {
10841093
// FIXME: We can't just compare the name of the symbol - it ignores the namespace
10851094
const isRawType =
10861095
!symbol ||
1087-
this.tc.getFullyQualifiedName(symbol) === "Date" ||
1088-
symbol.name === "integer" ||
1089-
this.tc.getIndexInfoOfType(typ, ts.IndexKind.Number) !== undefined;
1096+
// Window is incorrectly marked as rawType here for some reason
1097+
(this.tc.getFullyQualifiedName(symbol) !== "Window" &&
1098+
(this.tc.getFullyQualifiedName(symbol) === "Date" ||
1099+
symbol.name === "integer" ||
1100+
this.tc.getIndexInfoOfType(typ, ts.IndexKind.Number) !== undefined));
10901101

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

@@ -1116,15 +1128,16 @@ export class JsonSchemaGenerator {
11161128
reffedType!.getFlags() & ts.SymbolFlags.Alias ? this.tc.getAliasedSymbol(reffedType!) : reffedType!
11171129
)
11181130
.replace(REGEX_FILE_NAME_OR_SPACE, "");
1119-
if (this.args.uniqueNames) {
1120-
const sourceFile = getSourceFile(reffedType!);
1131+
if (this.args.uniqueNames && reffedType) {
1132+
const sourceFile = getSourceFile(reffedType);
11211133
const relativePath = path.relative(process.cwd(), sourceFile.fileName);
11221134
fullTypeName = `${typeName}.${generateHashOfNode(getCanonicalDeclaration(reffedType!), relativePath)}`;
11231135
} else {
11241136
fullTypeName = this.makeTypeNameUnique(typ, typeName);
11251137
}
1126-
} else if (asRef) {
1127-
if (this.args.uniqueNames) {
1138+
} else {
1139+
// typ.symbol can be undefined
1140+
if (this.args.uniqueNames && typ.symbol) {
11281141
const sym = typ.symbol;
11291142
const sourceFile = getSourceFile(sym);
11301143
const relativePath = path.relative(process.cwd(), sourceFile.fileName);
@@ -1139,6 +1152,15 @@ export class JsonSchemaGenerator {
11391152
}
11401153
}
11411154

1155+
// Handle recursive types
1156+
if (!isRawType || !!typ.aliasSymbol) {
1157+
if (this.recursiveTypeRef.has(fullTypeName)) {
1158+
asRef = true;
1159+
} else {
1160+
this.recursiveTypeRef.set(fullTypeName, definition);
1161+
}
1162+
}
1163+
11421164
if (asRef) {
11431165
// We don't return the full definition, but we put it into
11441166
// reffedDefinitions below.
@@ -1227,6 +1249,26 @@ export class JsonSchemaGenerator {
12271249
}
12281250
}
12291251

1252+
if (this.recursiveTypeRef.get(fullTypeName) === definition) {
1253+
this.recursiveTypeRef.delete(fullTypeName);
1254+
// If the type was recursive (there is reffedDefinitions) - lets replace it to reference
1255+
if (this.reffedDefinitions[fullTypeName]) {
1256+
// Here we may want to filter out all type specific fields
1257+
// and include fields like description etc
1258+
const annotations = Object.entries(returnedDefinition).reduce((acc, [key, value]) => {
1259+
if (validationKeywords[key] && typeof value !== undefined) {
1260+
acc[key] = value;
1261+
}
1262+
return acc;
1263+
}, {});
1264+
1265+
returnedDefinition = {
1266+
$ref: `${this.args.id}#/definitions/` + fullTypeName,
1267+
...annotations,
1268+
};
1269+
}
1270+
}
1271+
12301272
if (otherAnnotations["nullable"]) {
12311273
makeNullable(returnedDefinition);
12321274
}

0 commit comments

Comments
 (0)