diff --git a/.changeset/tame-months-knock.md b/.changeset/tame-months-knock.md new file mode 100644 index 0000000000..5f2d1457b2 --- /dev/null +++ b/.changeset/tame-months-knock.md @@ -0,0 +1,5 @@ +--- +'@gitbook/react-openapi': patch +--- + +Fix merge logic for allOf alternatives diff --git a/packages/react-openapi/src/OpenAPISchema.tsx b/packages/react-openapi/src/OpenAPISchema.tsx index 7c5e71d812..9761b54386 100644 --- a/packages/react-openapi/src/OpenAPISchema.tsx +++ b/packages/react-openapi/src/OpenAPISchema.tsx @@ -515,9 +515,42 @@ export function getSchemaAlternatives( ); } +/** + * Determine if a schema is safe to merge based on its properties + */ +function isSafeToMerge(schema: OpenAPIV3.SchemaObject): boolean { + const keys = Object.keys(schema); + + const coreProperties = ['type', 'properties', 'required', 'nullable']; + const safeExtensions = [ + 'description', + 'title', + 'example', + 'examples', + 'default', + 'readOnly', + 'writeOnly', + 'deprecated', + ]; + + const coreKeys = keys.filter((key) => coreProperties.includes(key)); + const unknownKeys = keys.filter( + (key) => + !coreProperties.includes(key) && !safeExtensions.includes(key) && !key.startsWith('x-') + ); + + // If there are no core properties or unknown properties, the schema is not safe to merge + if (coreKeys.length === 0 || unknownKeys.length > 0) { + return false; + } + + return true; +} + /** * Merge alternatives of the same type into a single schema. * - Merge string enums + * - Safely merge object schemas with compatible properties */ function mergeAlternatives( alternativeType: AlternativeType, @@ -565,24 +598,50 @@ function mergeAlternatives( if (latest && latest.type === 'object' && schemaOrRef.type === 'object') { const keys = Object.keys(schemaOrRef); - if ( - keys.every((key) => - ['type', 'properties', 'required', 'nullable'].includes(key) - ) - ) { + + if (isSafeToMerge(schemaOrRef)) { + const safeExtensions = [ + 'description', + 'title', + 'example', + 'examples', + 'default', + 'readOnly', + 'writeOnly', + 'deprecated', + ]; + const safeKeys = keys.filter((key) => safeExtensions.includes(key)); + const vendorKeys = keys.filter((key) => key.startsWith('x-')); + latest.properties = { - ...latest.properties, - ...schemaOrRef.properties, + ...(latest.properties || {}), + ...(schemaOrRef.properties || {}), }; latest.required = Array.from( new Set([ - ...(Array.isArray(latest.required) ? latest.required : []), - ...(Array.isArray(schemaOrRef.required) + ...(latest.required && Array.isArray(latest.required) + ? latest.required + : []), + ...(schemaOrRef.required && Array.isArray(schemaOrRef.required) ? schemaOrRef.required : []), ]) ); latest.nullable = latest.nullable || schemaOrRef.nullable; + + // Preserve safe extensions and vendor extensions + // Always overwrite (last schema has priority) + [...vendorKeys, ...safeKeys].forEach((key) => { + if ( + typeof latest[key] === 'object' && + typeof schemaOrRef[key] === 'object' + ) { + latest[key] = { ...latest[key], ...schemaOrRef[key] }; + } else { + latest[key] = schemaOrRef[key]; + } + }); + return acc; } }