Skip to content

Commit c6c364e

Browse files
committed
Fix OpenAPI OneOf/allOf merge
1 parent 27b9f78 commit c6c364e

File tree

2 files changed

+104
-32
lines changed

2 files changed

+104
-32
lines changed

packages/react-openapi/src/OpenAPISchema.tsx

Lines changed: 88 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ export function OpenAPISchemaPresentation(props: {
382382
<div id={id} className="openapi-schema-presentation">
383383
<OpenAPISchemaName
384384
schema={schema}
385-
type={getSchemaTitle(schema)}
385+
type={getSchemaTitle(schema, { ignoreAlternatives: !propertyName })}
386386
propertyName={propertyName}
387387
isDiscriminatorProperty={isDiscriminatorProperty}
388388
required={required}
@@ -686,36 +686,102 @@ function flattenAlternatives(
686686
schemasOrRefs: (OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject)[],
687687
ancestors: Set<OpenAPIV3.SchemaObject>
688688
): OpenAPIV3.SchemaObject[] {
689-
// Get the parent schema's required fields from the most recent ancestor
690689
const latestAncestor = Array.from(ancestors).pop();
690+
const result: OpenAPIV3.SchemaObject[] = [];
691691

692-
return schemasOrRefs.reduce<OpenAPIV3.SchemaObject[]>((acc, schemaOrRef) => {
692+
for (const schemaOrRef of schemasOrRefs) {
693693
if (checkIsReference(schemaOrRef)) {
694-
return acc;
694+
continue;
695695
}
696696

697-
if (schemaOrRef[alternativeType] && !ancestors.has(schemaOrRef)) {
698-
const alternatives = getSchemaAlternatives(schemaOrRef, ancestors);
699-
if (alternatives?.schemas) {
700-
acc.push(
701-
...alternatives.schemas.map((schema) => ({
702-
...schema,
703-
required: mergeRequiredFields(schema, latestAncestor),
704-
}))
705-
);
697+
const flattened = flattenSchema(schemaOrRef, alternativeType, ancestors, latestAncestor);
698+
699+
if (flattened) {
700+
result.push(...flattened);
701+
}
702+
}
703+
704+
return result;
705+
}
706+
707+
function flattenSchema(
708+
schema: OpenAPIV3.SchemaObject,
709+
alternativeType: AlternativeType,
710+
ancestors: Set<OpenAPIV3.SchemaObject>,
711+
latestAncestor: OpenAPIV3.SchemaObject | undefined
712+
): OpenAPIV3.SchemaObject[] {
713+
if (schema[alternativeType] && !ancestors.has(schema)) {
714+
const alternatives = getSchemaAlternatives(schema, ancestors);
715+
if (alternatives?.schemas && alternatives.type === alternativeType) {
716+
return alternatives.schemas.map((s) => ({
717+
...s,
718+
required: mergeRequiredFields(s, latestAncestor),
719+
}));
720+
}
721+
722+
return [
723+
{
724+
...schema,
725+
required: mergeRequiredFields(schema, latestAncestor),
726+
},
727+
];
728+
}
729+
730+
if (
731+
(alternativeType === 'oneOf' || alternativeType === 'anyOf') &&
732+
schema.allOf &&
733+
Array.isArray(schema.allOf) &&
734+
!ancestors.has(schema)
735+
) {
736+
const allOfSchemas = schema.allOf.filter(
737+
(s): s is OpenAPIV3.SchemaObject => !checkIsReference(s)
738+
);
739+
740+
if (allOfSchemas.length > 0) {
741+
const merged = mergeAlternatives('allOf', allOfSchemas);
742+
if (merged && merged.length > 0) {
743+
return merged.map((s) => {
744+
const required = mergeRequiredFields(s, latestAncestor);
745+
const result: OpenAPIV3.SchemaObject = {
746+
...s,
747+
...(required !== undefined && { required }),
748+
};
749+
750+
if (schema.title && !s.title) {
751+
result.title = schema.title;
752+
}
753+
754+
return result;
755+
});
706756
}
707-
return acc;
708757
}
758+
}
709759

710-
// For direct schemas, handle required fields
711-
const schema = {
712-
...schemaOrRef,
713-
required: mergeRequiredFields(schemaOrRef, latestAncestor),
714-
};
760+
const title = inferSchemaTitle(schema);
761+
const required = mergeRequiredFields(schema, latestAncestor);
762+
763+
return [
764+
{
765+
...schema,
766+
...(required !== undefined && { required }),
767+
...(title ? { title } : {}),
768+
},
769+
];
770+
}
771+
772+
function inferSchemaTitle(schema: OpenAPIV3.SchemaObject): string | undefined {
773+
if (schema.title) {
774+
return schema.title;
775+
}
776+
777+
if (schema.properties) {
778+
const keys = Object.keys(schema.properties);
779+
if (keys.length > 0) {
780+
return keys[0];
781+
}
782+
}
715783

716-
acc.push(schema);
717-
return acc;
718-
}, []);
784+
return undefined;
719785
}
720786

721787
/**

packages/react-openapi/src/utils.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -218,15 +218,18 @@ function getStatusCodeCategory(statusCode: number | string): number | string {
218218
return category;
219219
}
220220

221-
export function getSchemaTitle(schema: OpenAPIV3.SchemaObject): string {
221+
export function getSchemaTitle(
222+
schema: OpenAPIV3.SchemaObject,
223+
options?: { ignoreAlternatives?: boolean }
224+
): string {
222225
// Otherwise try to infer a nice title
223226
let type = 'any';
224227

225228
if (schema.enum || schema['x-enumDescriptions'] || schema['x-gitbook-enum']) {
226229
type = `${schema.type} · enum`;
227230
// check array AND schema.items as this is sometimes null despite what the type indicates
228231
} else if (schema.type === 'array' && !!schema.items) {
229-
type = `${getSchemaTitle(schema.items)}[]`;
232+
type = `${getSchemaTitle(schema.items, options)}[]`;
230233
} else if (Array.isArray(schema.type)) {
231234
type = schema.type.join(' | ');
232235
} else if (schema.type || schema.properties) {
@@ -242,14 +245,17 @@ export function getSchemaTitle(schema: OpenAPIV3.SchemaObject): string {
242245
}
243246
}
244247

245-
if ('anyOf' in schema) {
246-
type = 'any of';
247-
} else if ('oneOf' in schema) {
248-
type = 'one of';
249-
} else if ('allOf' in schema) {
250-
type = 'all of';
251-
} else if ('not' in schema) {
252-
type = 'not';
248+
// Skip alternative type labels if ignoreAlternatives is true (useful when rendering alternatives)
249+
if (!options?.ignoreAlternatives) {
250+
if ('anyOf' in schema) {
251+
type = 'any of';
252+
} else if ('oneOf' in schema) {
253+
type = 'one of';
254+
} else if ('allOf' in schema) {
255+
type = 'all of';
256+
} else if ('not' in schema) {
257+
type = 'not';
258+
}
253259
}
254260

255261
return type;

0 commit comments

Comments
 (0)