diff --git a/.changeset/eager-zoos-judge.md b/.changeset/eager-zoos-judge.md new file mode 100644 index 0000000000..07fc200fc3 --- /dev/null +++ b/.changeset/eager-zoos-judge.md @@ -0,0 +1,5 @@ +--- +'@gitbook/react-openapi': patch +--- + +Enhance discriminator handling in OpenAPISchema diff --git a/packages/react-openapi/src/OpenAPISchema.tsx b/packages/react-openapi/src/OpenAPISchema.tsx index e9aff7df34..fa7de90699 100644 --- a/packages/react-openapi/src/OpenAPISchema.tsx +++ b/packages/react-openapi/src/OpenAPISchema.tsx @@ -36,9 +36,19 @@ function OpenAPISchemaProperty( context: OpenAPIClientContext; circularRefs: CircularRefsIds; className?: string; + discriminator?: OpenAPIV3.DiscriminatorObject; + discriminatorValue?: string; } & Omit, '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; @@ -59,7 +69,7 @@ function OpenAPISchemaProperty( const circularRefs = new Map(parentCircularRefs); circularRefs.set(schema, id); - const properties = getSchemaProperties(schema); + const properties = getSchemaProperties(schema, discriminator, discriminatorValue); const ancestors = new Set(circularRefs.keys()); const alternatives = getSchemaAlternatives(schema, ancestors); @@ -67,26 +77,15 @@ function OpenAPISchemaProperty( const header = ; const content = (() => { if (alternatives?.schemas) { - const { schemas, discriminator } = alternatives; return ( -
- {schemas.map((alternativeSchema, index) => ( -
- - {index < schemas.length - 1 ? ( - - ) : null} -
- ))} -
+ ); } @@ -187,11 +186,30 @@ function OpenAPIRootSchema(props: { const id = useId(); const properties = getSchemaProperties(schema); const description = resolveDescription(schema); + const ancestors = new Set(parentCircularRefs.keys()); + const alternatives = getSchemaAlternatives(schema, ancestors); - if (properties?.length) { - const circularRefs = new Map(parentCircularRefs); - circularRefs.set(schema, id); + const circularRefs = new Map(parentCircularRefs); + circularRefs.set(schema, id); + + // Handle root-level oneOf/allOf/anyOf + if (alternatives?.schemas) { + return ( + <> + {description ? ( + + ) : null} + + + ); + } + if (properties?.length) { return ( <> {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 ( +
+ {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 ( +
+ + {index < schemas.length - 1 ? ( + + ) : null} +
+ ); + })} +
+ ); +} + /** * Render a tab for an alternative schema. * It renders directly the properties if relevant; @@ -236,11 +364,14 @@ export function OpenAPIRootSchemaFromServer(props: { function OpenAPISchemaAlternative(props: { schema: OpenAPIV3.SchemaObject; discriminator: OpenAPIV3.DiscriminatorObject | undefined; + discriminatorValue?: string; circularRefs: CircularRefsIds; context: OpenAPIClientContext; }) { 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 ? ( @@ -435,12 +568,13 @@ export function OpenAPISchemaPresentation(props: { */ function getSchemaProperties( schema: OpenAPIV3.SchemaObject, - discriminator?: OpenAPIV3.DiscriminatorObject | undefined + discriminator?: OpenAPIV3.DiscriminatorObject | undefined, + discriminatorValue?: string | undefined ): null | OpenAPISchemaPropertyEntry[] { // check array AND schema.items as this is sometimes null despite what the type indicates if (schema.type === 'array' && schema.items && !checkIsReference(schema.items)) { const items = schema.items; - const itemProperties = getSchemaProperties(items); + const itemProperties = getSchemaProperties(items, discriminator, discriminatorValue); if (itemProperties) { return itemProperties.map((prop) => ({ ...prop, @@ -467,8 +601,20 @@ function getSchemaProperties( if (schema.properties) { Object.entries(schema.properties).forEach(([propertyName, propertySchema]) => { + const isDiscriminator = discriminator?.propertyName === propertyName; if (checkIsReference(propertySchema)) { - return; + if (!isDiscriminator || !discriminatorValue) { + return; + } + } + + let finalSchema = propertySchema; + if (isDiscriminator && discriminatorValue) { + finalSchema = { + ...propertySchema, + const: discriminatorValue, + enum: [discriminatorValue], + }; } result.push({ @@ -476,8 +622,8 @@ function getSchemaProperties( required: Array.isArray(schema.required) ? schema.required.includes(propertyName) : undefined, - isDiscriminatorProperty: discriminator?.propertyName === propertyName, - schema: propertySchema, + isDiscriminatorProperty: isDiscriminator, + schema: finalSchema, }); }); }