1- import { unique } from 'remeda' ;
1+ import { isNullish , unique } from 'remeda' ;
22
33import { resolveExampleRefs , resolveObject } from '../resolvers' ;
44import {
55 type ContextSpec ,
66 EnumGeneration ,
77 type GeneratorImport ,
88 type GeneratorSchema ,
9+ type OpenApiReferenceObject ,
910 type OpenApiSchemaObject ,
1011 type ScalarValue ,
1112 SchemaType ,
@@ -35,19 +36,85 @@ type CombinedData = {
3536} ;
3637
3738type Separator = 'allOf' | 'anyOf' | 'oneOf' ;
39+ const mergeableAllOfKeys = new Set ( [ 'type' , 'properties' , 'required' ] ) ;
40+
41+ function isMergeableAllOfObject ( schema : OpenApiSchemaObject ) : boolean {
42+ // Must have properties to be worth merging
43+ if ( isNullish ( schema . properties ) ) {
44+ return false ;
45+ }
46+
47+ // Cannot merge if it contains nested composition
48+ if ( schema . allOf || schema . anyOf || schema . oneOf ) {
49+ return false ;
50+ }
51+
52+ // Only object types can be merged
53+ if ( ! isNullish ( schema . type ) && schema . type !== 'object' ) {
54+ return false ;
55+ }
56+
57+ // Only merge schemas with safe keys (type, properties, required)
58+ return Object . keys ( schema ) . every ( ( key ) => mergeableAllOfKeys . has ( key ) ) ;
59+ }
60+
61+ function normalizeAllOfSchema (
62+ schema : OpenApiSchemaObject ,
63+ ) : OpenApiSchemaObject {
64+ if ( ! schema . allOf ) {
65+ return schema ;
66+ }
67+
68+ let didMerge = false ;
69+ const mergedProperties = { ...schema . properties } ;
70+ const mergedRequired = new Set ( schema . required ) ;
71+ const remainingAllOf : ( OpenApiSchemaObject | OpenApiReferenceObject ) [ ] = [ ] ;
72+
73+ for ( const subSchema of schema . allOf ) {
74+ if ( isSchema ( subSchema ) && isMergeableAllOfObject ( subSchema ) ) {
75+ didMerge = true ;
76+ if ( subSchema . properties ) {
77+ Object . assign ( mergedProperties , subSchema . properties ) ;
78+ }
79+ if ( subSchema . required ) {
80+ for ( const prop of subSchema . required ) {
81+ mergedRequired . add ( prop ) ;
82+ }
83+ }
84+ continue ;
85+ }
86+
87+ remainingAllOf . push ( subSchema ) ;
88+ }
89+
90+ if ( ! didMerge || remainingAllOf . length === 0 ) {
91+ return schema ;
92+ }
93+
94+ return {
95+ ...schema ,
96+ ...( Object . keys ( mergedProperties ) . length > 0 && {
97+ properties : mergedProperties ,
98+ } ) ,
99+ ...( mergedRequired . size > 0 && { required : [ ...mergedRequired ] } ) ,
100+ ...( remainingAllOf . length > 0 && { allOf : remainingAllOf } ) ,
101+ } ;
102+ }
38103
39104interface CombineValuesOptions {
40105 resolvedData : CombinedData ;
41106 resolvedValue ?: ScalarValue ;
42107 separator : Separator ;
43108 context : ContextSpec ;
109+ parentSchema ?: OpenApiSchemaObject ;
44110}
45111
46112function combineValues ( {
47113 resolvedData,
48114 resolvedValue,
49115 separator,
50116 context,
117+ parentSchema,
51118} : CombineValuesOptions ) {
52119 const isAllEnums = resolvedData . isEnum . every ( Boolean ) ;
53120
@@ -89,6 +156,10 @@ function combineValues({
89156 ! resolvedData . originalSchema . some (
90157 ( schema ) =>
91158 schema ?. properties ?. [ prop ] && schema . required ?. includes ( prop ) ,
159+ ) &&
160+ ! (
161+ parentSchema ?. properties ?. [ prop ] &&
162+ parentSchema . required ?. includes ( prop )
92163 ) ,
93164 ) ;
94165 if ( overrideRequiredProperties . length > 0 ) {
@@ -147,9 +218,21 @@ export function combineSchemas({
147218 nullable : string ;
148219 formDataContext ?: FormDataContext ;
149220} ) : ScalarValue {
150- const items = schema [ separator ] ?? [ ] ;
221+ // Normalize allOf schemas by merging inline objects into parent (fixes #2458)
222+ // Only applies when: using allOf, not in v7 compat mode, no sibling oneOf/anyOf
223+ const canMergeInlineAllOf =
224+ separator === 'allOf' &&
225+ ! context . output . override . aliasCombinedTypes &&
226+ ! schema . oneOf &&
227+ ! schema . anyOf ;
228+
229+ const normalizedSchema = canMergeInlineAllOf
230+ ? normalizeAllOfSchema ( schema )
231+ : schema ;
232+
233+ const items = normalizedSchema [ separator ] ?? [ ] ;
151234
152- const resolvedData : CombinedData [ ] = items . reduce < CombinedData > (
235+ const resolvedData : CombinedData = items . reduce < CombinedData > (
153236 ( acc , subSchema ) => {
154237 // aliasCombinedTypes (v7 compat): create intermediate types like ResponseAnyOf
155238 // v8 default: propName stays undefined so combined types are inlined directly
@@ -283,13 +366,14 @@ export function combineSchemas({
283366
284367 let resolvedValue : ScalarValue | undefined ;
285368
286- if ( schema . properties ) {
369+ if ( normalizedSchema . properties ) {
287370 resolvedValue = getScalar ( {
288371 item : Object . fromEntries (
289- Object . entries ( schema ) . filter ( ( [ key ] ) => key !== separator ) ,
372+ Object . entries ( normalizedSchema ) . filter ( ( [ key ] ) => key !== separator ) ,
290373 ) ,
291374 name,
292375 context,
376+ formDataContext,
293377 } ) ;
294378 } else if ( separator === 'allOf' && ( schema . oneOf || schema . anyOf ) ) {
295379 // Handle sibling pattern: allOf + oneOf/anyOf at same level
@@ -309,6 +393,7 @@ export function combineSchemas({
309393 separator,
310394 resolvedValue,
311395 context,
396+ parentSchema : normalizedSchema ,
312397 } ) ;
313398
314399 return {
@@ -326,9 +411,7 @@ export function combineSchemas({
326411 type : 'object' as SchemaType ,
327412 isRef : false ,
328413 hasReadonlyProps :
329- resolvedData ?. hasReadonlyProps ||
330- resolvedValue ?. hasReadonlyProps ||
331- false ,
414+ resolvedData . hasReadonlyProps || resolvedValue ?. hasReadonlyProps || false ,
332415 example : schema . example ,
333416 examples : resolveExampleRefs ( schema . examples , context ) ,
334417 } ;
0 commit comments