Skip to content

Commit 0d8c81c

Browse files
committed
Support multiple discriminator values pointing to the same refs in allOf composition
1 parent b645047 commit 0d8c81c

File tree

3 files changed

+135
-75
lines changed

3 files changed

+135
-75
lines changed

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

+109-58
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,67 @@ function createDiscriminatorEnum(
184184
};
185185
}
186186

187+
function patchDiscriminatorEnum(
188+
schema: SchemaObject,
189+
ref: string,
190+
values: string[],
191+
discriminator: DiscriminatorObject,
192+
discriminatorRef: string,
193+
options: OpenAPITSOptions,
194+
): boolean {
195+
const resolvedSchema = resolveRef<SchemaObject>(schema, ref, {
196+
silent: options.silent ?? false,
197+
});
198+
199+
if (resolvedSchema?.allOf) {
200+
// if the schema is an allOf, we can append a new schema object to the allOf array
201+
resolvedSchema.allOf.push({
202+
type: "object",
203+
// discriminator enum properties always need to be required
204+
required: [discriminator.propertyName],
205+
properties: {
206+
[discriminator.propertyName]: createDiscriminatorEnum(values),
207+
},
208+
});
209+
210+
return true;
211+
} else if (
212+
typeof resolvedSchema === "object" &&
213+
"type" in resolvedSchema &&
214+
resolvedSchema.type === "object"
215+
) {
216+
// if the schema is an object, we can apply the discriminator enums to its properties
217+
if (!resolvedSchema.properties) {
218+
resolvedSchema.properties = {};
219+
}
220+
221+
// discriminator enum properties always need to be required
222+
if (!resolvedSchema.required) {
223+
resolvedSchema.required = [discriminator.propertyName];
224+
} else if (!resolvedSchema.required.includes(discriminator.propertyName)) {
225+
resolvedSchema.required.push(discriminator.propertyName);
226+
}
227+
228+
// add/replace the discriminator enum property
229+
resolvedSchema.properties[discriminator.propertyName] =
230+
createDiscriminatorEnum(
231+
values,
232+
resolvedSchema.properties[discriminator.propertyName],
233+
);
234+
235+
return true;
236+
}
237+
238+
warn(
239+
`Discriminator mapping has an invalid schema (neither an object schema nor an allOf array): ${ref} => ${values.join(
240+
", ",
241+
)} (Discriminator: ${discriminatorRef})`,
242+
options.silent,
243+
);
244+
245+
return false;
246+
}
247+
187248
type InternalDiscriminatorMapping = Record<
188249
string,
189250
{ inferred?: string; defined?: string[] }
@@ -267,57 +328,18 @@ export function scanDiscriminators(
267328
// the inferred enum values from the schema might not represent the actual enum values of the discriminator,
268329
// so if we have defined values, use them instead
269330
const mappedValues = defined ?? [inferred!];
270-
const resolvedSchema = resolveRef<SchemaObject>(schema, mappedRef, {
271-
silent: options.silent ?? false,
272-
});
273-
274-
if (resolvedSchema?.allOf) {
275-
// if the schema is an allOf, we can append a new schema object to the allOf array
276-
resolvedSchema.allOf.push({
277-
type: "object",
278-
// discriminator enum properties always need to be required
279-
required: [discriminator.propertyName],
280-
properties: {
281-
[discriminator.propertyName]: createDiscriminatorEnum(mappedValues),
282-
},
283-
});
284331

285-
refsHandled.push(mappedRef);
286-
} else if (
287-
typeof resolvedSchema === "object" &&
288-
"type" in resolvedSchema &&
289-
resolvedSchema.type === "object"
332+
if (
333+
patchDiscriminatorEnum(
334+
schema,
335+
mappedRef,
336+
mappedValues,
337+
discriminator,
338+
ref,
339+
options,
340+
)
290341
) {
291-
// if the schema is an object, we can apply the discriminator enums to its properties
292-
if (!resolvedSchema.properties) {
293-
resolvedSchema.properties = {};
294-
}
295-
296-
// discriminator enum properties always need to be required
297-
if (!resolvedSchema.required) {
298-
resolvedSchema.required = [discriminator.propertyName];
299-
} else if (
300-
!resolvedSchema.required.includes(discriminator.propertyName)
301-
) {
302-
resolvedSchema.required.push(discriminator.propertyName);
303-
}
304-
305-
// add/replace the discriminator enum property
306-
resolvedSchema.properties[discriminator.propertyName] =
307-
createDiscriminatorEnum(
308-
mappedValues,
309-
resolvedSchema.properties[discriminator.propertyName],
310-
);
311-
312342
refsHandled.push(mappedRef);
313-
} else {
314-
warn(
315-
`Discriminator mapping has an invalid schema (neither an object schema nor an allOf array): ${mappedRef} => ${mappedValues.join(
316-
", ",
317-
)} (Discriminator: ${ref})`,
318-
options.silent,
319-
);
320-
continue;
321343
}
322344
}
323345
});
@@ -326,19 +348,48 @@ export function scanDiscriminators(
326348
// (sometimes this mapping is implicit, so it can’t be done until we know
327349
// about every discriminator in the document)
328350
walk(schema, (obj, path) => {
329-
for (const key of ["allOf"] as const) {
330-
if (obj && Array.isArray(obj[key])) {
331-
for (const item of (obj as any)[key]) {
332-
if ("$ref" in item) {
333-
if (objects[item.$ref]) {
334-
objects[createRef(path)] = {
335-
...objects[item.$ref],
336-
};
351+
if (!obj || !Array.isArray(obj.allOf)) {
352+
return;
353+
}
354+
355+
for (const item of (obj as any).allOf) {
356+
if ("$ref" in item) {
357+
if (!objects[item.$ref]) {
358+
return;
359+
}
360+
361+
const ref = createRef(path);
362+
const discriminator = objects[item.$ref];
363+
const mappedValues: string[] = [];
364+
365+
if (discriminator.mapping) {
366+
for (const mappedValue in discriminator.mapping) {
367+
if (discriminator.mapping[mappedValue] === ref) {
368+
mappedValues.push(mappedValue);
369+
}
370+
}
371+
372+
if (mappedValues.length > 0) {
373+
if (
374+
patchDiscriminatorEnum(
375+
schema,
376+
ref,
377+
mappedValues,
378+
discriminator,
379+
item.$ref,
380+
options,
381+
)
382+
) {
383+
refsHandled.push(ref);
337384
}
338-
} else if (item.discriminator?.propertyName) {
339-
objects[createRef(path)] = { ...item.discriminator };
340385
}
341386
}
387+
388+
objects[ref] = {
389+
...objects[item.$ref],
390+
};
391+
} else if (item.discriminator?.propertyName) {
392+
objects[createRef(path)] = { ...item.discriminator };
342393
}
343394
}
344395
});

packages/openapi-typescript/src/transform/schema-object.ts

+17-13
Original file line numberDiff line numberDiff line change
@@ -162,24 +162,25 @@ export function transformSchemaObjectWithComposition(
162162
itemType = transformSchemaObject(item, options);
163163

164164
const resolved = options.ctx.resolve<SchemaObject>(item.$ref);
165+
166+
// make keys required, if necessary
165167
if (
166168
resolved &&
167169
typeof resolved === "object" &&
168-
"properties" in resolved
170+
"properties" in resolved &&
171+
// we have already handled this item (discriminator property was already added as required)
172+
!options.ctx.discriminators.refsHandled.includes(item.$ref)
169173
) {
170-
// don’t try and make keys required if we have already handled the item (discriminator property was already added as required)
171-
// or the $ref doesn’t have them
172-
if (!options.ctx.discriminators.refsHandled.includes(item.$ref)) {
173-
const validRequired = (required ?? []).filter(
174-
(key) => !!resolved.properties![key],
174+
// add WithRequired<X, Y> if necessary
175+
const validRequired = (required ?? []).filter(
176+
(key) => !!resolved.properties![key],
177+
);
178+
if (validRequired.length) {
179+
itemType = tsWithRequired(
180+
itemType,
181+
validRequired,
182+
options.ctx.injectFooter,
175183
);
176-
if (validRequired.length) {
177-
itemType = tsWithRequired(
178-
itemType,
179-
validRequired,
180-
options.ctx.injectFooter,
181-
);
182-
}
183184
}
184185
}
185186
}
@@ -423,8 +424,11 @@ function transformSchemaObjectCore(
423424
// for all magic inheritance, we will have already gathered it into
424425
// ctx.discriminators. But stop objects from referencing their own
425426
// discriminator meant for children (!schemaObject.discriminator)
427+
// and don't add discriminator properties if we already added/patched
428+
// them (options.ctx.discriminators.refsHandled.includes(options.path!).
426429
const discriminator =
427430
!schemaObject.discriminator &&
431+
!options.ctx.discriminators.refsHandled.includes(options.path!) &&
428432
options.ctx.discriminators.objects[options.path!];
429433
if (discriminator) {
430434
coreObjectType.unshift(

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

+9-4
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ describe("3.1 discriminators", () => {
2020
propertyName: "petType",
2121
mapping: {
2222
dog: "#/components/schemas/Dog",
23+
poodle: "#/components/schemas/Dog",
2324
},
2425
},
2526
},
@@ -88,11 +89,15 @@ export interface components {
8889
Cat: {
8990
petType: "Cat";
9091
} & Omit<components["schemas"]["Pet"], "petType">;
91-
Dog: {
92-
petType: "dog";
93-
} & (Omit<components["schemas"]["Pet"], "petType"> & {
92+
Dog: Omit<components["schemas"]["Pet"], "petType"> & {
9493
bark?: string;
95-
});
94+
} & {
95+
/**
96+
* @description discriminator enum property added by openapi-typescript
97+
* @enum {string}
98+
*/
99+
petType: "dog" | "poodle";
100+
};
96101
Lizard: {
97102
petType: "Lizard";
98103
lovesRocks?: boolean;

0 commit comments

Comments
 (0)