Skip to content

Commit 40efe3d

Browse files
committed
Support discriminator enum mappings using the same ref multiple times
1 parent b786954 commit 40efe3d

File tree

2 files changed

+57
-16
lines changed

2 files changed

+57
-16
lines changed

packages/openapi-typescript/src/lib/utils.ts

+52-13
Original file line numberDiff line numberDiff line change
@@ -171,14 +171,24 @@ export function resolveRef<T>(
171171
return node;
172172
}
173173

174-
function createDiscriminatorEnum(value: string): SchemaObject {
174+
function createDiscriminatorEnum(
175+
values: string[],
176+
prevSchema?: SchemaObject,
177+
): SchemaObject {
175178
return {
176179
type: "string",
177-
enum: [value],
178-
description: `discriminator enum property added by openapi-typescript`,
180+
enum: values,
181+
description: prevSchema?.description
182+
? `${prevSchema.description} (enum property replaced by openapi-typescript)`
183+
: `discriminator enum property added by openapi-typescript`,
179184
};
180185
}
181186

187+
type InternalDiscriminatorMapping = Record<
188+
string,
189+
{ inferred?: string; defined?: string[] }
190+
>;
191+
182192
/** Return a key–value map of discriminator objects found in a schema */
183193
export function scanDiscriminators(
184194
schema: OpenAPI3,
@@ -197,7 +207,7 @@ export function scanDiscriminators(
197207
return;
198208
}
199209

200-
// add to discriminators object for later usage
210+
// collect discriminator object for later usage
201211
const ref = createRef(path);
202212

203213
objects[ref] = discriminator;
@@ -209,16 +219,20 @@ export function scanDiscriminators(
209219
}
210220

211221
const oneOf: (SchemaObject | ReferenceObject)[] = obj.oneOf;
212-
const mapping: Record<string, string> = {};
222+
const mapping: InternalDiscriminatorMapping = {};
213223

214224
// the mapping can be inferred from the oneOf refs next to the discriminator object
215225
for (const item of oneOf) {
216226
if ("$ref" in item) {
217-
// the name of the schema is the infered discriminator enum value
227+
// the name of the schema is the inferred discriminator enum value
218228
const value = item.$ref.split("/").pop();
219229

220230
if (value) {
221-
mapping[item.$ref] = value;
231+
if (!mapping[item.$ref]) {
232+
mapping[item.$ref] = { inferred: value };
233+
} else {
234+
mapping[item.$ref].inferred = value;
235+
}
222236
}
223237
}
224238
}
@@ -227,26 +241,44 @@ export function scanDiscriminators(
227241
if (discriminator.mapping) {
228242
for (const mappedValue in discriminator.mapping) {
229243
const mappedRef = discriminator.mapping[mappedValue];
244+
if (!mappedRef) {
245+
continue;
246+
}
247+
248+
if (!mapping[mappedRef]?.defined) {
249+
// this overrides inferred values, but we don't need them anymore as soon as we have a defined value
250+
mapping[mappedRef] = { defined: [] };
251+
}
230252

231-
mapping[mappedRef] = mappedValue;
253+
mapping[mappedRef].defined?.push(mappedValue);
232254
}
233255
}
234256

235-
for (const [mappedRef, mappedValue] of Object.entries(mapping)) {
257+
for (const [mappedRef, { inferred, defined }] of Object.entries(mapping)) {
236258
if (refsHandled.includes(mappedRef)) {
237259
continue;
238260
}
239261

262+
if (!inferred && !defined) {
263+
continue;
264+
}
265+
266+
// prefer defined values over automatically inferred ones
267+
// the inferred enum values from the schema might not represent the actual enum values of the discriminator,
268+
// so if we have defined values, use them instead
269+
const mappedValues = defined ?? [inferred!];
240270
const resolvedSchema = resolveRef<SchemaObject>(schema, mappedRef, {
241271
silent: options.silent ?? false,
242272
});
273+
243274
if (resolvedSchema?.allOf) {
244275
// if the schema is an allOf, we can append a new schema object to the allOf array
245276
resolvedSchema.allOf.push({
246277
type: "object",
278+
// discriminator enum properties always need to be required
247279
required: [discriminator.propertyName],
248280
properties: {
249-
[discriminator.propertyName]: createDiscriminatorEnum(mappedValue),
281+
[discriminator.propertyName]: createDiscriminatorEnum(mappedValues),
250282
},
251283
});
252284

@@ -256,11 +288,12 @@ export function scanDiscriminators(
256288
"type" in resolvedSchema &&
257289
resolvedSchema.type === "object"
258290
) {
259-
// if the schema is an object, we can add/replace the discriminator enum to it
291+
// if the schema is an object, we can apply the discriminator enums to its properties
260292
if (!resolvedSchema.properties) {
261293
resolvedSchema.properties = {};
262294
}
263295

296+
// discriminator enum properties always need to be required
264297
if (!resolvedSchema.required) {
265298
resolvedSchema.required = [discriminator.propertyName];
266299
} else if (
@@ -269,13 +302,19 @@ export function scanDiscriminators(
269302
resolvedSchema.required.push(discriminator.propertyName);
270303
}
271304

305+
// add/replace the discriminator enum property
272306
resolvedSchema.properties[discriminator.propertyName] =
273-
createDiscriminatorEnum(mappedValue);
307+
createDiscriminatorEnum(
308+
mappedValues,
309+
resolvedSchema.properties[discriminator.propertyName],
310+
);
274311

275312
refsHandled.push(mappedRef);
276313
} else {
277314
warn(
278-
`Discriminator mapping has an invalid schema (neither an object schema nor an allOf array): ${mappedRef} => ${mappedValue} (Discriminator: ${ref})`,
315+
`Discriminator mapping has an invalid schema (neither an object schema nor an allOf array): ${mappedRef} => ${mappedValues.join(
316+
", ",
317+
)} (Discriminator: ${ref})`,
279318
options.silent,
280319
);
281320
continue;

packages/openapi-typescript/test/discriminators.test.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,8 @@ export type operations = Record<string, never>;`,
454454
mapping: {
455455
simple: "#/components/schemas/simpleObject",
456456
complex: "#/components/schemas/complexObject",
457+
// special case for different enum value but using the same schema as 'complex'
458+
tooComplex: "#/components/schemas/complexObject",
457459
},
458460
},
459461
},
@@ -507,7 +509,7 @@ export type operations = Record<string, never>;`,
507509
},
508510
type: {
509511
type: "string",
510-
enum: ["simple", "complex"],
512+
enum: ["simple", "complex", "tooComplex"],
511513
},
512514
},
513515
},
@@ -571,10 +573,10 @@ export interface components {
571573
* @description discriminator enum property added by openapi-typescript
572574
* @enum {string}
573575
*/
574-
type: "complex";
576+
type: "complex" | "tooComplex";
575577
};
576578
/** @enum {string} */
577-
type: "simple" | "complex";
579+
type: "simple" | "complex" | "tooComplex";
578580
};
579581
responses: never;
580582
parameters: never;

0 commit comments

Comments
 (0)