Skip to content

Commit 82e628d

Browse files
committed
Enhance discriminator handling in OpenAPISchema
1 parent 6b43773 commit 82e628d

File tree

1 file changed

+156
-26
lines changed

1 file changed

+156
-26
lines changed

packages/react-openapi/src/OpenAPISchema.tsx

Lines changed: 156 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,19 @@ function OpenAPISchemaProperty(
3636
context: OpenAPIClientContext;
3737
circularRefs: CircularRefsIds;
3838
className?: string;
39+
discriminator?: OpenAPIV3.DiscriminatorObject;
40+
discriminatorValue?: string;
3941
} & Omit<ComponentPropsWithoutRef<'div'>, 'property' | 'context' | 'circularRefs' | 'className'>
4042
) {
41-
const { circularRefs: parentCircularRefs, context, className, property, ...rest } = props;
43+
const {
44+
circularRefs: parentCircularRefs,
45+
context,
46+
className,
47+
property,
48+
discriminator,
49+
discriminatorValue,
50+
...rest
51+
} = props;
4252

4353
const { schema } = property;
4454

@@ -59,33 +69,47 @@ function OpenAPISchemaProperty(
5969
const circularRefs = new Map(parentCircularRefs);
6070
circularRefs.set(schema, id);
6171

62-
const properties = getSchemaProperties(schema);
72+
const properties = getSchemaProperties(schema, discriminator, discriminatorValue);
6373

6474
const ancestors = new Set(circularRefs.keys());
6575
const alternatives = getSchemaAlternatives(schema, ancestors);
6676

6777
const header = <OpenAPISchemaPresentation id={id} context={context} property={property} />;
6878
const content = (() => {
6979
if (alternatives?.schemas) {
70-
const { schemas, discriminator } = alternatives;
80+
const { schemas, discriminator: alternativeDiscriminator, type } = alternatives;
7181
return (
7282
<div className="openapi-schema-alternatives">
73-
{schemas.map((alternativeSchema, index) => (
74-
<div key={index} className="openapi-schema-alternative">
75-
<OpenAPISchemaAlternative
76-
schema={alternativeSchema}
77-
discriminator={discriminator}
78-
circularRefs={circularRefs}
79-
context={context}
80-
/>
81-
{index < schemas.length - 1 ? (
82-
<OpenAPISchemaAlternativeSeparator
83-
schema={schema}
83+
{schemas.map((alternativeSchema, index) => {
84+
// If the alternative has its own discriminator, use it.
85+
const effectiveDiscriminator =
86+
alternativeDiscriminator ||
87+
(type === 'allOf' ? discriminator : undefined);
88+
89+
// If we are inheriting and using parent discriminator, pass down the value.
90+
const effectiveDiscriminatorValue =
91+
!alternativeDiscriminator && type === 'allOf'
92+
? discriminatorValue
93+
: undefined;
94+
95+
return (
96+
<div key={index} className="openapi-schema-alternative">
97+
<OpenAPISchemaAlternative
98+
schema={alternativeSchema}
99+
discriminator={effectiveDiscriminator}
100+
discriminatorValue={effectiveDiscriminatorValue}
101+
circularRefs={circularRefs}
84102
context={context}
85103
/>
86-
) : null}
87-
</div>
88-
))}
104+
{index < schemas.length - 1 ? (
105+
<OpenAPISchemaAlternativeSeparator
106+
schema={schema}
107+
context={context}
108+
/>
109+
) : null}
110+
</div>
111+
);
112+
})}
89113
</div>
90114
);
91115
}
@@ -187,11 +211,49 @@ function OpenAPIRootSchema(props: {
187211
const id = useId();
188212
const properties = getSchemaProperties(schema);
189213
const description = resolveDescription(schema);
214+
const ancestors = new Set(parentCircularRefs.keys());
215+
const alternatives = getSchemaAlternatives(schema, ancestors);
190216

191-
if (properties?.length) {
192-
const circularRefs = new Map(parentCircularRefs);
193-
circularRefs.set(schema, id);
217+
const circularRefs = new Map(parentCircularRefs);
218+
circularRefs.set(schema, id);
219+
220+
// Handle root-level oneOf/allOf/anyOf
221+
if (alternatives?.schemas) {
222+
const { schemas, discriminator: alternativeDiscriminator, type } = alternatives;
223+
return (
224+
<>
225+
{description ? (
226+
<Markdown source={description} className="openapi-schema-root-description" />
227+
) : null}
228+
<div className="openapi-schema-alternatives">
229+
{schemas.map((alternativeSchema, index) => {
230+
// If the alternative has its own discriminator, use it.
231+
const effectiveDiscriminator =
232+
alternativeDiscriminator || (type === 'allOf' ? undefined : undefined);
233+
234+
return (
235+
<div key={index} className="openapi-schema-alternative">
236+
<OpenAPISchemaAlternative
237+
schema={alternativeSchema}
238+
discriminator={effectiveDiscriminator}
239+
circularRefs={circularRefs}
240+
context={context}
241+
/>
242+
{index < schemas.length - 1 ? (
243+
<OpenAPISchemaAlternativeSeparator
244+
schema={schema}
245+
context={context}
246+
/>
247+
) : null}
248+
</div>
249+
);
250+
})}
251+
</div>
252+
</>
253+
);
254+
}
194255

256+
if (properties?.length) {
195257
return (
196258
<>
197259
{description ? (
@@ -228,6 +290,56 @@ export function OpenAPIRootSchemaFromServer(props: {
228290
);
229291
}
230292

293+
/**
294+
* Get the discriminator value for a schema.
295+
*/
296+
function getDiscriminatorValue(
297+
schema: OpenAPIV3.SchemaObject,
298+
discriminator: OpenAPIV3.DiscriminatorObject | undefined
299+
): string | undefined {
300+
if (!discriminator) {
301+
return undefined;
302+
}
303+
304+
if (discriminator.mapping) {
305+
const mappingEntry = Object.entries(discriminator.mapping).find(([key, ref]) => {
306+
if (schema.title === ref || (!!schema.title && ref.endsWith(`/${schema.title}`))) {
307+
return true;
308+
}
309+
310+
// Fallback: check if the title contains the key (normalized)
311+
if (schema.title?.toLowerCase().replace(/\s/g, '').includes(key.toLowerCase())) {
312+
return true;
313+
}
314+
315+
return false;
316+
});
317+
318+
if (mappingEntry) {
319+
return mappingEntry[0];
320+
}
321+
}
322+
323+
if (!discriminator.propertyName || !schema.properties) {
324+
return undefined;
325+
}
326+
327+
const property = schema.properties[discriminator.propertyName];
328+
if (!property || checkIsReference(property)) {
329+
return undefined;
330+
}
331+
332+
if (property.const) {
333+
return String(property.const);
334+
}
335+
336+
if (property.enum?.length === 1) {
337+
return String(property.enum[0]);
338+
}
339+
340+
return;
341+
}
342+
231343
/**
232344
* Render a tab for an alternative schema.
233345
* It renders directly the properties if relevant;
@@ -236,11 +348,14 @@ export function OpenAPIRootSchemaFromServer(props: {
236348
function OpenAPISchemaAlternative(props: {
237349
schema: OpenAPIV3.SchemaObject;
238350
discriminator: OpenAPIV3.DiscriminatorObject | undefined;
351+
discriminatorValue?: string;
239352
circularRefs: CircularRefsIds;
240353
context: OpenAPIClientContext;
241354
}) {
242355
const { schema, discriminator, circularRefs, context } = props;
243-
const properties = getSchemaProperties(schema, discriminator);
356+
const discriminatorValue =
357+
props.discriminatorValue || getDiscriminatorValue(schema, discriminator);
358+
const properties = getSchemaProperties(schema, discriminator, discriminatorValue);
244359

245360
return properties?.length ? (
246361
<OpenAPIDisclosure
@@ -257,6 +372,8 @@ function OpenAPISchemaAlternative(props: {
257372
) : (
258373
<OpenAPISchemaProperty
259374
property={{ schema }}
375+
discriminator={discriminator}
376+
discriminatorValue={discriminatorValue}
260377
circularRefs={circularRefs}
261378
context={context}
262379
/>
@@ -435,12 +552,13 @@ export function OpenAPISchemaPresentation(props: {
435552
*/
436553
function getSchemaProperties(
437554
schema: OpenAPIV3.SchemaObject,
438-
discriminator?: OpenAPIV3.DiscriminatorObject | undefined
555+
discriminator?: OpenAPIV3.DiscriminatorObject | undefined,
556+
discriminatorValue?: string | undefined
439557
): null | OpenAPISchemaPropertyEntry[] {
440558
// check array AND schema.items as this is sometimes null despite what the type indicates
441559
if (schema.type === 'array' && schema.items && !checkIsReference(schema.items)) {
442560
const items = schema.items;
443-
const itemProperties = getSchemaProperties(items);
561+
const itemProperties = getSchemaProperties(items, discriminator, discriminatorValue);
444562
if (itemProperties) {
445563
return itemProperties.map((prop) => ({
446564
...prop,
@@ -467,17 +585,29 @@ function getSchemaProperties(
467585

468586
if (schema.properties) {
469587
Object.entries(schema.properties).forEach(([propertyName, propertySchema]) => {
588+
const isDiscriminator = discriminator?.propertyName === propertyName;
470589
if (checkIsReference(propertySchema)) {
471-
return;
590+
if (!isDiscriminator || !discriminatorValue) {
591+
return;
592+
}
593+
}
594+
595+
let finalSchema = propertySchema;
596+
if (isDiscriminator && discriminatorValue) {
597+
finalSchema = {
598+
...propertySchema,
599+
const: discriminatorValue,
600+
enum: [discriminatorValue],
601+
};
472602
}
473603

474604
result.push({
475605
propertyName,
476606
required: Array.isArray(schema.required)
477607
? schema.required.includes(propertyName)
478608
: undefined,
479-
isDiscriminatorProperty: discriminator?.propertyName === propertyName,
480-
schema: propertySchema,
609+
isDiscriminatorProperty: isDiscriminator,
610+
schema: finalSchema,
481611
});
482612
});
483613
}

0 commit comments

Comments
 (0)