Skip to content

Commit 8761cee

Browse files
authored
Enhance discriminator handling in OpenAPISchema (#3855)
1 parent 44feb3b commit 8761cee

File tree

2 files changed

+181
-30
lines changed

2 files changed

+181
-30
lines changed

.changeset/eager-zoos-judge.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@gitbook/react-openapi': patch
3+
---
4+
5+
Enhance discriminator handling in OpenAPISchema

packages/react-openapi/src/OpenAPISchema.tsx

Lines changed: 176 additions & 30 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,34 +69,23 @@ 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;
7180
return (
72-
<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}
84-
context={context}
85-
/>
86-
) : null}
87-
</div>
88-
))}
89-
</div>
81+
<OpenAPISchemaAlternatives
82+
alternatives={alternatives}
83+
schema={schema}
84+
circularRefs={circularRefs}
85+
context={context}
86+
parentDiscriminator={discriminator}
87+
parentDiscriminatorValue={discriminatorValue}
88+
/>
9089
);
9190
}
9291

@@ -187,11 +186,30 @@ function OpenAPIRootSchema(props: {
187186
const id = useId();
188187
const properties = getSchemaProperties(schema);
189188
const description = resolveDescription(schema);
189+
const ancestors = new Set(parentCircularRefs.keys());
190+
const alternatives = getSchemaAlternatives(schema, ancestors);
190191

191-
if (properties?.length) {
192-
const circularRefs = new Map(parentCircularRefs);
193-
circularRefs.set(schema, id);
192+
const circularRefs = new Map(parentCircularRefs);
193+
circularRefs.set(schema, id);
194+
195+
// Handle root-level oneOf/allOf/anyOf
196+
if (alternatives?.schemas) {
197+
return (
198+
<>
199+
{description ? (
200+
<Markdown source={description} className="openapi-schema-root-description" />
201+
) : null}
202+
<OpenAPISchemaAlternatives
203+
alternatives={alternatives}
204+
schema={schema}
205+
circularRefs={circularRefs}
206+
context={context}
207+
/>
208+
</>
209+
);
210+
}
194211

212+
if (properties?.length) {
195213
return (
196214
<>
197215
{description ? (
@@ -228,6 +246,116 @@ export function OpenAPIRootSchemaFromServer(props: {
228246
);
229247
}
230248

249+
/**
250+
* Get the discriminator value for a schema.
251+
*/
252+
function getDiscriminatorValue(
253+
schema: OpenAPIV3.SchemaObject,
254+
discriminator: OpenAPIV3.DiscriminatorObject | undefined
255+
): string | undefined {
256+
if (!discriminator) {
257+
return undefined;
258+
}
259+
260+
if (discriminator.mapping) {
261+
const mappingEntry = Object.entries(discriminator.mapping).find(([key, ref]) => {
262+
if (schema.title === ref || (!!schema.title && ref.endsWith(`/${schema.title}`))) {
263+
return true;
264+
}
265+
266+
// Fallback: check if the title contains the key (normalized)
267+
if (schema.title?.toLowerCase().replace(/\s/g, '').includes(key.toLowerCase())) {
268+
return true;
269+
}
270+
271+
return false;
272+
});
273+
274+
if (mappingEntry) {
275+
return mappingEntry[0];
276+
}
277+
}
278+
279+
if (!discriminator.propertyName || !schema.properties) {
280+
return undefined;
281+
}
282+
283+
const property = schema.properties[discriminator.propertyName];
284+
if (!property || checkIsReference(property)) {
285+
return undefined;
286+
}
287+
288+
if (property.const) {
289+
return String(property.const);
290+
}
291+
292+
if (property.enum?.length === 1) {
293+
return String(property.enum[0]);
294+
}
295+
296+
return;
297+
}
298+
299+
/**
300+
* Render alternatives (oneOf/allOf/anyOf) for a schema.
301+
*/
302+
function OpenAPISchemaAlternatives(props: {
303+
alternatives: SchemaAlternatives;
304+
schema: OpenAPIV3.SchemaObject;
305+
circularRefs: CircularRefsIds;
306+
context: OpenAPIClientContext;
307+
parentDiscriminator?: OpenAPIV3.DiscriminatorObject;
308+
parentDiscriminatorValue?: string;
309+
}) {
310+
const {
311+
alternatives,
312+
schema,
313+
circularRefs,
314+
context,
315+
parentDiscriminator,
316+
parentDiscriminatorValue,
317+
} = props;
318+
319+
if (!alternatives?.schemas) {
320+
return null;
321+
}
322+
323+
const { schemas, discriminator: alternativeDiscriminator, type } = alternatives;
324+
325+
return (
326+
<div className="openapi-schema-alternatives">
327+
{schemas.map((alternativeSchema, index) => {
328+
// If the alternative has its own discriminator, use it.
329+
// Otherwise, for allOf, inherit from parent discriminator.
330+
const effectiveDiscriminator =
331+
alternativeDiscriminator ||
332+
(type === 'allOf' ? parentDiscriminator : undefined);
333+
334+
// If we are inheriting and using parent discriminator, pass down the value.
335+
const effectiveDiscriminatorValue =
336+
!alternativeDiscriminator && type === 'allOf'
337+
? parentDiscriminatorValue
338+
: undefined;
339+
340+
return (
341+
<div key={index} className="openapi-schema-alternative">
342+
<OpenAPISchemaAlternative
343+
schema={alternativeSchema}
344+
discriminator={effectiveDiscriminator}
345+
discriminatorValue={effectiveDiscriminatorValue}
346+
circularRefs={circularRefs}
347+
context={context}
348+
/>
349+
{index < schemas.length - 1 ? (
350+
<OpenAPISchemaAlternativeSeparator schema={schema} context={context} />
351+
) : null}
352+
</div>
353+
);
354+
})}
355+
</div>
356+
);
357+
}
358+
231359
/**
232360
* Render a tab for an alternative schema.
233361
* It renders directly the properties if relevant;
@@ -236,11 +364,14 @@ export function OpenAPIRootSchemaFromServer(props: {
236364
function OpenAPISchemaAlternative(props: {
237365
schema: OpenAPIV3.SchemaObject;
238366
discriminator: OpenAPIV3.DiscriminatorObject | undefined;
367+
discriminatorValue?: string;
239368
circularRefs: CircularRefsIds;
240369
context: OpenAPIClientContext;
241370
}) {
242371
const { schema, discriminator, circularRefs, context } = props;
243-
const properties = getSchemaProperties(schema, discriminator);
372+
const discriminatorValue =
373+
props.discriminatorValue || getDiscriminatorValue(schema, discriminator);
374+
const properties = getSchemaProperties(schema, discriminator, discriminatorValue);
244375

245376
return properties?.length ? (
246377
<OpenAPIDisclosure
@@ -257,6 +388,8 @@ function OpenAPISchemaAlternative(props: {
257388
) : (
258389
<OpenAPISchemaProperty
259390
property={{ schema }}
391+
discriminator={discriminator}
392+
discriminatorValue={discriminatorValue}
260393
circularRefs={circularRefs}
261394
context={context}
262395
/>
@@ -435,12 +568,13 @@ export function OpenAPISchemaPresentation(props: {
435568
*/
436569
function getSchemaProperties(
437570
schema: OpenAPIV3.SchemaObject,
438-
discriminator?: OpenAPIV3.DiscriminatorObject | undefined
571+
discriminator?: OpenAPIV3.DiscriminatorObject | undefined,
572+
discriminatorValue?: string | undefined
439573
): null | OpenAPISchemaPropertyEntry[] {
440574
// check array AND schema.items as this is sometimes null despite what the type indicates
441575
if (schema.type === 'array' && schema.items && !checkIsReference(schema.items)) {
442576
const items = schema.items;
443-
const itemProperties = getSchemaProperties(items);
577+
const itemProperties = getSchemaProperties(items, discriminator, discriminatorValue);
444578
if (itemProperties) {
445579
return itemProperties.map((prop) => ({
446580
...prop,
@@ -467,17 +601,29 @@ function getSchemaProperties(
467601

468602
if (schema.properties) {
469603
Object.entries(schema.properties).forEach(([propertyName, propertySchema]) => {
604+
const isDiscriminator = discriminator?.propertyName === propertyName;
470605
if (checkIsReference(propertySchema)) {
471-
return;
606+
if (!isDiscriminator || !discriminatorValue) {
607+
return;
608+
}
609+
}
610+
611+
let finalSchema = propertySchema;
612+
if (isDiscriminator && discriminatorValue) {
613+
finalSchema = {
614+
...propertySchema,
615+
const: discriminatorValue,
616+
enum: [discriminatorValue],
617+
};
472618
}
473619

474620
result.push({
475621
propertyName,
476622
required: Array.isArray(schema.required)
477623
? schema.required.includes(propertyName)
478624
: undefined,
479-
isDiscriminatorProperty: discriminator?.propertyName === propertyName,
480-
schema: propertySchema,
625+
isDiscriminatorProperty: isDiscriminator,
626+
schema: finalSchema,
481627
});
482628
});
483629
}

0 commit comments

Comments
 (0)