@@ -34,6 +34,7 @@ export class SchemaModifier {
3434 this . collapseOrMergeOneOfArray ( schema )
3535 this . collapseOneOfObjectPropContainsTitleSchema ( schema )
3636 this . removeArrayOfMapWrapper ( schema )
37+ this . convertOneOfToMinMaxProperties ( schema )
3738 } ,
3839 } ) ;
3940 const visit = new Set ( ) ;
@@ -477,6 +478,110 @@ export class SchemaModifier {
477478 }
478479 }
479480
481+ /**
482+ * Converts oneOf pattern with single-property objects into minProperties/maxProperties pattern.
483+ * This allows the template to recognize it as a oneof constraint.
484+ *
485+ * Example:
486+ * Input:
487+ * {
488+ * allOf: [
489+ * { properties: { meta: {...} } },
490+ * { oneOf: [
491+ * { properties: { field1: {...} }, required: [field1] },
492+ * { properties: { field2: {...} }, required: [field2] }
493+ * ]
494+ * }
495+ * ]
496+ * }
497+ *
498+ * Output:
499+ * {
500+ * allOf: [
501+ * { properties: { meta: {...} } },
502+ * {
503+ * properties: {
504+ * field1: {...},
505+ * field2: {...}
506+ * },
507+ * minProperties: 1,
508+ * maxProperties: 1
509+ * }
510+ * ]
511+ * }
512+ **/
513+ convertOneOfToMinMaxProperties ( schema : OpenAPIV3 . SchemaObject ) : void {
514+ // Check allOf/anyOf/oneOf for oneOf patterns
515+ const composedKeys = [ 'allOf' , 'anyOf' , 'oneOf' ] as const ;
516+
517+ for ( const key of composedKeys ) {
518+ const items = schema [ key ] ;
519+ if ( Array . isArray ( items ) ) {
520+ for ( let i = 0 ; i < items . length ; i ++ ) {
521+ const item = items [ i ] ;
522+ if ( item && typeof item === 'object' && ! ( '$ref' in item ) ) {
523+ const itemSchema = item as any ;
524+
525+ // Check if this item has a oneOf with single-property patterns
526+ if ( Array . isArray ( itemSchema . oneOf ) && itemSchema . oneOf . length > 0 ) {
527+ if ( this . isOneOfWithSingleProperties ( itemSchema . oneOf ) ) {
528+ // Convert oneOf to minProperties/maxProperties pattern
529+ this . flattenOneOfToProperties ( itemSchema ) ;
530+ this . logger . info ( `Converted oneOf pattern to minProperties/maxProperties` ) ;
531+ }
532+ }
533+ }
534+ }
535+ }
536+ }
537+ }
538+
539+ /**
540+ * Checks if oneOf items are all single-property objects
541+ **/
542+ private isOneOfWithSingleProperties ( oneOfItems : any [ ] ) : boolean {
543+ return oneOfItems . every ( item => {
544+ if ( ! item || typeof item !== 'object' || '$ref' in item ) {
545+ return false ;
546+ }
547+ // Check if item has exactly one property and a required field
548+ const hasProperties = item . properties && Object . keys ( item . properties ) . length === 1 ;
549+ const hasRequired = Array . isArray ( item . required ) && item . required . length === 1 ;
550+ return hasProperties && hasRequired ;
551+ } ) ;
552+ }
553+
554+ /**
555+ * Flattens oneOf structure into merged properties with minProperties/maxProperties
556+ **/
557+ private flattenOneOfToProperties ( schema : any ) : void {
558+ if ( ! Array . isArray ( schema . oneOf ) ) {
559+ return ;
560+ }
561+
562+ const mergedProperties : any = { } ;
563+
564+ // Merge all properties from each oneOf item
565+ for ( const item of schema . oneOf ) {
566+ if ( item && item . properties ) {
567+ Object . assign ( mergedProperties , item . properties ) ;
568+ }
569+ }
570+
571+ // Replace oneOf with merged properties and add constraints
572+ schema . properties = mergedProperties ;
573+ schema . minProperties = 1 ;
574+ schema . maxProperties = 1 ;
575+
576+ // Remove unevaluatedProperties if present
577+ if ( 'unevaluatedProperties' in schema ) {
578+ delete schema . unevaluatedProperties ;
579+ }
580+
581+ delete schema . oneOf ;
582+ delete schema . required ;
583+ }
584+
480585 /**
481586 * Marks schemas and properties with oneOf extensions.
482587 * Adds x-oneof-property to properties when schema has maxProperties=1.
0 commit comments