Skip to content

Commit dd00edd

Browse files
committed
fix #116
1 parent d2b1eac commit dd00edd

File tree

5 files changed

+134
-48
lines changed

5 files changed

+134
-48
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "json-schema-to-typescript",
3-
"version": "4.6.4",
3+
"version": "4.6.5",
44
"description": "compile json schema to typescript typings",
55
"main": "dist/src/index.js",
66
"bin": {

src/parser.ts

Lines changed: 62 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,19 @@ import { findKey, includes, isPlainObject, map } from 'lodash'
44
import { typeOfSchema } from './typeOfSchema'
55
import { AST, hasStandaloneName, T_ANY, T_ANY_ADDITIONAL_PROPERTIES, TInterface, TInterfaceParam, TNamedInterface } from './types/AST'
66
import { JSONSchema, JSONSchemaWithDefinitions, SchemaSchema } from './types/JSONSchema'
7-
import { error, log } from './utils'
7+
import { error, generateName, log } from './utils'
88

99
export type Processed = Map<JSONSchema | JSONSchema4Type, AST>
1010

11+
export type UsedNames = Set<string>
12+
1113
export function parse(
1214
schema: JSONSchema | JSONSchema4Type,
1315
rootSchema = schema as JSONSchema,
1416
keyName?: string,
1517
isSchema = true,
16-
processed: Processed = new Map<JSONSchema | JSONSchema4Type, AST>()
18+
processed: Processed = new Map<JSONSchema | JSONSchema4Type, AST>(),
19+
usedNames = new Set<string>()
1720
): AST {
1821

1922
// If we've seen this node before, return it.
@@ -32,7 +35,7 @@ export function parse(
3235
const set = (_ast: AST) => Object.assign(ast, _ast)
3336

3437
return isSchema
35-
? parseNonLiteral(schema as SchemaSchema, rootSchema, keyName, keyNameFromDefinition, set, processed)
38+
? parseNonLiteral(schema as SchemaSchema, rootSchema, keyName, keyNameFromDefinition, set, processed, usedNames)
3639
: parseLiteral(schema, keyName, keyNameFromDefinition, set)
3740
}
3841

@@ -56,7 +59,8 @@ function parseNonLiteral(
5659
keyName: string | undefined,
5760
keyNameFromDefinition: string | undefined,
5861
set: (ast: AST) => AST,
59-
processed: Processed
62+
processed: Processed,
63+
usedNames: UsedNames
6064
) {
6165

6266
log(whiteBright.bgBlue('parser'), schema, '<-' + typeOfSchema(schema), processed.has(schema) ? '(FROM CACHE)' : '')
@@ -66,72 +70,72 @@ function parseNonLiteral(
6670
return set({
6771
comment: schema.description,
6872
keyName,
69-
params: schema.allOf!.map(_ => parse(_, rootSchema, undefined, true, processed)),
70-
standaloneName: schema.title || schema.id || keyNameFromDefinition,
73+
params: schema.allOf!.map(_ => parse(_, rootSchema, undefined, true, processed, usedNames)),
74+
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
7175
type: 'INTERSECTION'
7276
})
7377
case 'ANY':
7478
return set({
7579
comment: schema.description,
7680
keyName,
77-
standaloneName: schema.title || schema.id || keyNameFromDefinition,
81+
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
7882
type: 'ANY'
7983
})
8084
case 'ANY_OF':
8185
return set({
8286
comment: schema.description,
8387
keyName,
84-
params: schema.anyOf!.map(_ => parse(_, rootSchema, undefined, true, processed)),
85-
standaloneName: schema.title || schema.id || keyNameFromDefinition,
88+
params: schema.anyOf!.map(_ => parse(_, rootSchema, undefined, true, processed, usedNames)),
89+
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
8690
type: 'UNION'
8791
})
8892
case 'BOOLEAN':
8993
return set({
9094
comment: schema.description,
9195
keyName,
92-
standaloneName: schema.title || schema.id || keyNameFromDefinition,
96+
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
9397
type: 'BOOLEAN'
9498
})
9599
case 'NAMED_ENUM':
96100
return set({
97101
comment: schema.description,
98102
keyName,
99103
params: schema.enum!.map((_, n) => ({
100-
ast: parse(_, rootSchema, undefined, false, processed),
104+
ast: parse(_, rootSchema, undefined, false, processed, usedNames),
101105
keyName: schema.tsEnumNames![n]
102106
})),
103-
standaloneName: schema.title || keyName!,
107+
standaloneName: standaloneName(schema, keyName, usedNames)!,
104108
type: 'ENUM'
105109
})
106110
case 'NAMED_SCHEMA':
107-
return set(newInterface(schema as SchemaSchema, rootSchema, processed, keyName))
111+
return set(newInterface(schema as SchemaSchema, rootSchema, processed, usedNames, keyName))
108112
case 'NULL':
109113
return set({
110114
comment: schema.description,
111115
keyName,
112-
standaloneName: schema.title || schema.id || keyNameFromDefinition,
116+
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
113117
type: 'NULL'
114118
})
115119
case 'NUMBER':
116120
return set({
117121
comment: schema.description,
118122
keyName,
119-
standaloneName: schema.title || schema.id || keyNameFromDefinition,
123+
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
120124
type: 'NUMBER'
121125
})
122126
case 'OBJECT':
123127
return set({
124128
comment: schema.description,
125129
keyName,
126-
standaloneName: schema.title || schema.id || keyNameFromDefinition,
130+
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
127131
type: 'OBJECT'
128132
})
129133
case 'ONE_OF':
130134
return set({
131135
comment: schema.description,
132136
keyName,
133-
params: schema.oneOf!.map(_ => parse(_, rootSchema, undefined, true, processed)),
134-
standaloneName: schema.title || schema.id || keyNameFromDefinition,
137+
params: schema.oneOf!.map(_ => parse(_, rootSchema, undefined, true, processed, usedNames)),
138+
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
135139
type: 'UNION'
136140
})
137141
case 'REFERENCE':
@@ -140,76 +144,92 @@ function parseNonLiteral(
140144
return set({
141145
comment: schema.description,
142146
keyName,
143-
standaloneName: schema.title || schema.id || keyNameFromDefinition,
147+
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
144148
type: 'STRING'
145149
})
146150
case 'TYPED_ARRAY':
147151
if (Array.isArray(schema.items)) {
148152
return set({
149153
comment: schema.description,
150154
keyName,
151-
params: schema.items.map(_ => parse(_, rootSchema, undefined, true, processed)),
152-
standaloneName: schema.title || schema.id || keyNameFromDefinition,
155+
params: schema.items.map(_ => parse(_, rootSchema, undefined, true, processed, usedNames)),
156+
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
153157
type: 'TUPLE'
154158
})
155159
} else {
156160
return set({
157161
comment: schema.description,
158162
keyName,
159-
params: parse(schema.items!, rootSchema, undefined, true, processed),
160-
standaloneName: schema.title || schema.id || keyNameFromDefinition,
163+
params: parse(schema.items!, rootSchema, undefined, true, processed, usedNames),
164+
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
161165
type: 'ARRAY'
162166
})
163167
}
164168
case 'UNION':
165169
return set({
166170
comment: schema.description,
167171
keyName,
168-
params: (schema.type as JSONSchema4TypeName[]).map(_ => parse({ type: _ }, rootSchema, undefined, true, processed)),
169-
standaloneName: schema.title || schema.id || keyNameFromDefinition,
172+
params: (schema.type as JSONSchema4TypeName[]).map(_ => parse({ type: _ }, rootSchema, undefined, true, processed, usedNames)),
173+
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
170174
type: 'UNION'
171175
})
172176
case 'UNNAMED_ENUM':
173177
return set({
174178
comment: schema.description,
175179
keyName,
176-
params: schema.enum!.map(_ => parse(_, rootSchema, undefined, false, processed)),
177-
standaloneName: schema.title || schema.id || keyNameFromDefinition,
180+
params: schema.enum!.map(_ => parse(_, rootSchema, undefined, false, processed, usedNames)),
181+
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
178182
type: 'UNION'
179183
})
180184
case 'UNNAMED_SCHEMA':
181-
return set(newInterface(schema as SchemaSchema, rootSchema, processed, keyName, keyNameFromDefinition))
185+
return set(newInterface(schema as SchemaSchema, rootSchema, processed, usedNames, keyName, keyNameFromDefinition))
182186
case 'UNTYPED_ARRAY':
183187
return set({
184188
comment: schema.description,
185189
keyName,
186190
params: T_ANY,
187-
standaloneName: schema.title || schema.id || keyNameFromDefinition,
191+
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
188192
type: 'ARRAY'
189193
})
190194
}
191195
}
192196

197+
/**
198+
* Compute a schema name using a series of fallbacks
199+
*/
200+
function standaloneName(
201+
schema: JSONSchema,
202+
keyNameFromDefinition: string | undefined,
203+
usedNames: UsedNames
204+
) {
205+
let name = schema.title || schema.id || keyNameFromDefinition
206+
if (name) {
207+
return generateName(name, usedNames)
208+
}
209+
}
210+
193211
function newInterface(
194212
schema: SchemaSchema,
195213
rootSchema: JSONSchema,
196214
processed: Processed,
215+
usedNames: UsedNames,
197216
keyName?: string,
198217
keyNameFromDefinition?: string
199218
): TInterface {
200219
return {
201220
comment: schema.description,
202221
keyName,
203-
params: parseSchema(schema, rootSchema, processed),
204-
standaloneName: computeSchemaName(schema) || keyNameFromDefinition,
205-
superTypes: parseSuperTypes(schema, processed),
222+
params: parseSchema(schema, rootSchema, processed, usedNames),
223+
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
224+
superTypes: parseSuperTypes(schema, processed, usedNames),
206225
type: 'INTERFACE'
207226
}
208227
}
209228

210229
function parseSuperTypes(
211230
schema: SchemaSchema,
212-
processed: Processed
231+
processed: Processed,
232+
usedNames: UsedNames
213233
): TNamedInterface[] {
214234
// Type assertion needed because of dereferencing step
215235
// TODO: Type it upstream
@@ -218,42 +238,37 @@ function parseSuperTypes(
218238
return []
219239
}
220240
if (Array.isArray(superTypes)) {
221-
return superTypes.map(_ => newNamedInterface(_, _, processed))
241+
return superTypes.map(_ => newNamedInterface(_, _, processed, usedNames))
222242
}
223-
return [newNamedInterface(superTypes, superTypes, processed)]
243+
return [newNamedInterface(superTypes, superTypes, processed, usedNames)]
224244
}
225245

226246
function newNamedInterface(
227247
schema: SchemaSchema,
228248
rootSchema: JSONSchema,
229-
processed: Processed
249+
processed: Processed,
250+
usedNames: UsedNames
230251
): TNamedInterface {
231-
const namedInterface = newInterface(schema, rootSchema, processed)
252+
const namedInterface = newInterface(schema, rootSchema, processed, usedNames)
232253
if (hasStandaloneName(namedInterface)) {
233254
return namedInterface
234255
}
235256
// TODO: Generate name if it doesn't have one
236257
throw error('Supertype must have standalone name!', namedInterface)
237258
}
238259

239-
/**
240-
* Compute a schema name using a series of fallbacks
241-
*/
242-
function computeSchemaName(schema: SchemaSchema): string | undefined {
243-
return schema.title || schema.id
244-
}
245-
246260
/**
247261
* Helper to parse schema properties into params on the parent schema's type
248262
*/
249263
function parseSchema(
250264
schema: SchemaSchema,
251265
rootSchema: JSONSchema,
252-
processed: Processed
266+
processed: Processed,
267+
usedNames: UsedNames
253268
): TInterfaceParam[] {
254269

255270
const asts = map(schema.properties, (value, key: string) => ({
256-
ast: parse(value, rootSchema, key, true, processed),
271+
ast: parse(value, rootSchema, key, true, processed, usedNames),
257272
isRequired: includes(schema.required || [], key),
258273
keyName: key
259274
}))
@@ -275,7 +290,7 @@ function parseSchema(
275290
// defined via index signatures are already optional
276291
default:
277292
return asts.concat({
278-
ast: parse(schema.additionalProperties, rootSchema, '[k: string]', true, processed),
293+
ast: parse(schema.additionalProperties, rootSchema, '[k: string]', true, processed, usedNames),
279294
isRequired: true,
280295
keyName: '[k: string]'
281296
})

src/utils.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,22 @@ export function toSafeString(string: string) {
5656
return upperFirst(camelCase(string))
5757
}
5858

59+
export function generateName(from: string, usedNames: Set<string>) {
60+
let name = toSafeString(from)
61+
62+
// increment counter until we find a free name
63+
if (usedNames.has(name)) {
64+
let counter = 1
65+
while (usedNames.has(name)) {
66+
name = `${toSafeString(from)}${counter}`
67+
counter++
68+
}
69+
}
70+
71+
usedNames.add(name)
72+
return name
73+
}
74+
5975
export function error(...messages: any[]) {
6076
console.error(whiteBright.bgRedBright('error'), ...messages)
6177
}

test/e2e/ref.5.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
export const input = {
2+
title: 'Referencing',
3+
type: 'object',
4+
properties: {
5+
foo: {
6+
$ref: 'ReferencedTypeWithoutID.json'
7+
},
8+
bar: {
9+
$ref: 'ReferencedTypeWithoutIDConflict.json'
10+
}
11+
},
12+
required: ['foo', 'bar'],
13+
additionalProperties: false
14+
}
15+
16+
export const options = {
17+
cwd: 'test/resources/'
18+
}
19+
20+
export const output = `/**
21+
* This file was automatically generated by json-schema-to-typescript.
22+
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
23+
* and run json-schema-to-typescript to regenerate this file.
24+
*/
25+
26+
export interface Referencing {
27+
foo: ExampleSchema;
28+
bar: ExampleSchema1;
29+
}
30+
export interface ExampleSchema {
31+
firstName: string;
32+
lastName: string;
33+
/**
34+
* Age in years
35+
*/
36+
age?: number;
37+
height?: number;
38+
favoriteFoods?: any[];
39+
likesDogs?: boolean;
40+
[k: string]: any;
41+
}
42+
export interface ExampleSchema1 {
43+
isConflict: boolean;
44+
}
45+
`
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"title": "Example Schema",
3+
"properties": {
4+
"isConflict": {
5+
"type": "boolean"
6+
}
7+
},
8+
"required": ["isConflict"],
9+
"additionalProperties": false
10+
}

0 commit comments

Comments
 (0)