Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/eager-zoos-judge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@gitbook/react-openapi': patch
---

Enhance discriminator handling in OpenAPISchema
206 changes: 176 additions & 30 deletions packages/react-openapi/src/OpenAPISchema.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,19 @@ function OpenAPISchemaProperty(
context: OpenAPIClientContext;
circularRefs: CircularRefsIds;
className?: string;
discriminator?: OpenAPIV3.DiscriminatorObject;
discriminatorValue?: string;
} & 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;

Expand All @@ -59,34 +69,23 @@ 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);

const header = <OpenAPISchemaPresentation id={id} context={context} property={property} />;
const content = (() => {
if (alternatives?.schemas) {
const { schemas, discriminator } = alternatives;
return (
<div className="openapi-schema-alternatives">
{schemas.map((alternativeSchema, index) => (
<div key={index} className="openapi-schema-alternative">
<OpenAPISchemaAlternative
schema={alternativeSchema}
discriminator={discriminator}
circularRefs={circularRefs}
context={context}
/>
{index < schemas.length - 1 ? (
<OpenAPISchemaAlternativeSeparator
schema={schema}
context={context}
/>
) : null}
</div>
))}
</div>
<OpenAPISchemaAlternatives
alternatives={alternatives}
schema={schema}
circularRefs={circularRefs}
context={context}
parentDiscriminator={discriminator}
parentDiscriminatorValue={discriminatorValue}
/>
);
}

Expand Down Expand Up @@ -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 ? (
<Markdown source={description} className="openapi-schema-root-description" />
) : null}
<OpenAPISchemaAlternatives
alternatives={alternatives}
schema={schema}
circularRefs={circularRefs}
context={context}
/>
</>
);
}

if (properties?.length) {
return (
<>
{description ? (
Expand Down Expand Up @@ -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.
* It renders directly the properties if relevant;
Expand All @@ -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 ? (
<OpenAPIDisclosure
Expand All @@ -257,6 +388,8 @@ function OpenAPISchemaAlternative(props: {
) : (
<OpenAPISchemaProperty
property={{ schema }}
discriminator={discriminator}
discriminatorValue={discriminatorValue}
circularRefs={circularRefs}
context={context}
/>
Expand Down Expand Up @@ -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,
Expand All @@ -467,17 +601,29 @@ 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({
propertyName,
required: Array.isArray(schema.required)
? schema.required.includes(propertyName)
: undefined,
isDiscriminatorProperty: discriminator?.propertyName === propertyName,
schema: propertySchema,
isDiscriminatorProperty: isDiscriminator,
schema: finalSchema,
});
});
}
Expand Down