Enhance discriminator handling in OpenAPISchema (#3855)

This commit is contained in:
Nolann B.
2025-12-11 08:11:14 +01:00
committed by GitHub
parent 44feb3b140
commit 8761cee1bb
2 changed files with 181 additions and 30 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-openapi': patch
---
Enhance discriminator handling in OpenAPISchema
+176 -30
View File
@@ -36,9 +36,19 @@ function OpenAPISchemaProperty(
context: OpenAPIClientContext; context: OpenAPIClientContext;
circularRefs: CircularRefsIds; circularRefs: CircularRefsIds;
className?: string; className?: string;
discriminator?: OpenAPIV3.DiscriminatorObject;
discriminatorValue?: string;
} & Omit<ComponentPropsWithoutRef<'div'>, 'property' | 'context' | 'circularRefs' | 'className'> } & Omit<ComponentPropsWithoutRef<'div'>, 'property' | 'context' | 'circularRefs' | 'className'>
) { ) {
const { circularRefs: parentCircularRefs, context, className, property, ...rest } = props; const {
circularRefs: parentCircularRefs,
context,
className,
property,
discriminator,
discriminatorValue,
...rest
} = props;
const { schema } = property; const { schema } = property;
@@ -59,7 +69,7 @@ function OpenAPISchemaProperty(
const circularRefs = new Map(parentCircularRefs); const circularRefs = new Map(parentCircularRefs);
circularRefs.set(schema, id); circularRefs.set(schema, id);
const properties = getSchemaProperties(schema); const properties = getSchemaProperties(schema, discriminator, discriminatorValue);
const ancestors = new Set(circularRefs.keys()); const ancestors = new Set(circularRefs.keys());
const alternatives = getSchemaAlternatives(schema, ancestors); const alternatives = getSchemaAlternatives(schema, ancestors);
@@ -67,26 +77,15 @@ function OpenAPISchemaProperty(
const header = <OpenAPISchemaPresentation id={id} context={context} property={property} />; const header = <OpenAPISchemaPresentation id={id} context={context} property={property} />;
const content = (() => { const content = (() => {
if (alternatives?.schemas) { if (alternatives?.schemas) {
const { schemas, discriminator } = alternatives;
return ( return (
<div className="openapi-schema-alternatives"> <OpenAPISchemaAlternatives
{schemas.map((alternativeSchema, index) => ( alternatives={alternatives}
<div key={index} className="openapi-schema-alternative"> schema={schema}
<OpenAPISchemaAlternative circularRefs={circularRefs}
schema={alternativeSchema} context={context}
discriminator={discriminator} parentDiscriminator={discriminator}
circularRefs={circularRefs} parentDiscriminatorValue={discriminatorValue}
context={context} />
/>
{index < schemas.length - 1 ? (
<OpenAPISchemaAlternativeSeparator
schema={schema}
context={context}
/>
) : null}
</div>
))}
</div>
); );
} }
@@ -187,11 +186,30 @@ function OpenAPIRootSchema(props: {
const id = useId(); const id = useId();
const properties = getSchemaProperties(schema); const properties = getSchemaProperties(schema);
const description = resolveDescription(schema); const description = resolveDescription(schema);
const ancestors = new Set(parentCircularRefs.keys());
const alternatives = getSchemaAlternatives(schema, ancestors);
const circularRefs = new Map(parentCircularRefs);
circularRefs.set(schema, id);
// Handle root-level oneOf/allOf/anyOf
if (alternatives?.schemas) {
return (
<>
{description ? (
<Markdown source={description} className="openapi-schema-root-description" />
) : null}
<OpenAPISchemaAlternatives
alternatives={alternatives}
schema={schema}
circularRefs={circularRefs}
context={context}
/>
</>
);
}
if (properties?.length) { if (properties?.length) {
const circularRefs = new Map(parentCircularRefs);
circularRefs.set(schema, id);
return ( return (
<> <>
{description ? ( {description ? (
@@ -228,6 +246,116 @@ export function OpenAPIRootSchemaFromServer(props: {
); );
} }
/**
* Get the discriminator value for a schema.
*/
function getDiscriminatorValue(
schema: OpenAPIV3.SchemaObject,
discriminator: OpenAPIV3.DiscriminatorObject | undefined
): string | undefined {
if (!discriminator) {
return undefined;
}
if (discriminator.mapping) {
const mappingEntry = Object.entries(discriminator.mapping).find(([key, ref]) => {
if (schema.title === ref || (!!schema.title && ref.endsWith(`/${schema.title}`))) {
return true;
}
// Fallback: check if the title contains the key (normalized)
if (schema.title?.toLowerCase().replace(/\s/g, '').includes(key.toLowerCase())) {
return true;
}
return false;
});
if (mappingEntry) {
return mappingEntry[0];
}
}
if (!discriminator.propertyName || !schema.properties) {
return undefined;
}
const property = schema.properties[discriminator.propertyName];
if (!property || checkIsReference(property)) {
return undefined;
}
if (property.const) {
return String(property.const);
}
if (property.enum?.length === 1) {
return String(property.enum[0]);
}
return;
}
/**
* Render alternatives (oneOf/allOf/anyOf) for a schema.
*/
function OpenAPISchemaAlternatives(props: {
alternatives: SchemaAlternatives;
schema: OpenAPIV3.SchemaObject;
circularRefs: CircularRefsIds;
context: OpenAPIClientContext;
parentDiscriminator?: OpenAPIV3.DiscriminatorObject;
parentDiscriminatorValue?: string;
}) {
const {
alternatives,
schema,
circularRefs,
context,
parentDiscriminator,
parentDiscriminatorValue,
} = props;
if (!alternatives?.schemas) {
return null;
}
const { schemas, discriminator: alternativeDiscriminator, type } = alternatives;
return (
<div className="openapi-schema-alternatives">
{schemas.map((alternativeSchema, index) => {
// If the alternative has its own discriminator, use it.
// Otherwise, for allOf, inherit from parent discriminator.
const effectiveDiscriminator =
alternativeDiscriminator ||
(type === 'allOf' ? parentDiscriminator : undefined);
// If we are inheriting and using parent discriminator, pass down the value.
const effectiveDiscriminatorValue =
!alternativeDiscriminator && type === 'allOf'
? parentDiscriminatorValue
: undefined;
return (
<div key={index} className="openapi-schema-alternative">
<OpenAPISchemaAlternative
schema={alternativeSchema}
discriminator={effectiveDiscriminator}
discriminatorValue={effectiveDiscriminatorValue}
circularRefs={circularRefs}
context={context}
/>
{index < schemas.length - 1 ? (
<OpenAPISchemaAlternativeSeparator schema={schema} context={context} />
) : null}
</div>
);
})}
</div>
);
}
/** /**
* Render a tab for an alternative schema. * Render a tab for an alternative schema.
* It renders directly the properties if relevant; * It renders directly the properties if relevant;
@@ -236,11 +364,14 @@ export function OpenAPIRootSchemaFromServer(props: {
function OpenAPISchemaAlternative(props: { function OpenAPISchemaAlternative(props: {
schema: OpenAPIV3.SchemaObject; schema: OpenAPIV3.SchemaObject;
discriminator: OpenAPIV3.DiscriminatorObject | undefined; discriminator: OpenAPIV3.DiscriminatorObject | undefined;
discriminatorValue?: string;
circularRefs: CircularRefsIds; circularRefs: CircularRefsIds;
context: OpenAPIClientContext; context: OpenAPIClientContext;
}) { }) {
const { schema, discriminator, circularRefs, context } = props; const { schema, discriminator, circularRefs, context } = props;
const properties = getSchemaProperties(schema, discriminator); const discriminatorValue =
props.discriminatorValue || getDiscriminatorValue(schema, discriminator);
const properties = getSchemaProperties(schema, discriminator, discriminatorValue);
return properties?.length ? ( return properties?.length ? (
<OpenAPIDisclosure <OpenAPIDisclosure
@@ -257,6 +388,8 @@ function OpenAPISchemaAlternative(props: {
) : ( ) : (
<OpenAPISchemaProperty <OpenAPISchemaProperty
property={{ schema }} property={{ schema }}
discriminator={discriminator}
discriminatorValue={discriminatorValue}
circularRefs={circularRefs} circularRefs={circularRefs}
context={context} context={context}
/> />
@@ -435,12 +568,13 @@ export function OpenAPISchemaPresentation(props: {
*/ */
function getSchemaProperties( function getSchemaProperties(
schema: OpenAPIV3.SchemaObject, schema: OpenAPIV3.SchemaObject,
discriminator?: OpenAPIV3.DiscriminatorObject | undefined discriminator?: OpenAPIV3.DiscriminatorObject | undefined,
discriminatorValue?: string | undefined
): null | OpenAPISchemaPropertyEntry[] { ): null | OpenAPISchemaPropertyEntry[] {
// check array AND schema.items as this is sometimes null despite what the type indicates // check array AND schema.items as this is sometimes null despite what the type indicates
if (schema.type === 'array' && schema.items && !checkIsReference(schema.items)) { if (schema.type === 'array' && schema.items && !checkIsReference(schema.items)) {
const items = schema.items; const items = schema.items;
const itemProperties = getSchemaProperties(items); const itemProperties = getSchemaProperties(items, discriminator, discriminatorValue);
if (itemProperties) { if (itemProperties) {
return itemProperties.map((prop) => ({ return itemProperties.map((prop) => ({
...prop, ...prop,
@@ -467,8 +601,20 @@ function getSchemaProperties(
if (schema.properties) { if (schema.properties) {
Object.entries(schema.properties).forEach(([propertyName, propertySchema]) => { Object.entries(schema.properties).forEach(([propertyName, propertySchema]) => {
const isDiscriminator = discriminator?.propertyName === propertyName;
if (checkIsReference(propertySchema)) { if (checkIsReference(propertySchema)) {
return; if (!isDiscriminator || !discriminatorValue) {
return;
}
}
let finalSchema = propertySchema;
if (isDiscriminator && discriminatorValue) {
finalSchema = {
...propertySchema,
const: discriminatorValue,
enum: [discriminatorValue],
};
} }
result.push({ result.push({
@@ -476,8 +622,8 @@ function getSchemaProperties(
required: Array.isArray(schema.required) required: Array.isArray(schema.required)
? schema.required.includes(propertyName) ? schema.required.includes(propertyName)
: undefined, : undefined,
isDiscriminatorProperty: discriminator?.propertyName === propertyName, isDiscriminatorProperty: isDiscriminator,
schema: propertySchema, schema: finalSchema,
}); });
}); });
} }