@@ -19,13 +19,15 @@ export class SchemaModifier {
1919 traverse ( this . root , {
2020 onSchemaProperty : ( schema ) => {
2121 this . deduplicateEnumValue ( schema )
22+ this . convertAdditionalPropertiesToProperty ( schema , true )
2223 this . handleAdditionalPropertiesUndefined ( schema )
2324 this . convertNullTypeToNullValue ( schema )
2425 this . collapseOrMergeOneOfArray ( schema )
2526 } ,
2627 onSchema : ( schema , schemaName ) => {
2728 if ( ! schema || isReferenceObject ( schema ) ) return ;
2829 this . deduplicateEnumValue ( schema )
30+ this . convertAdditionalPropertiesToProperty ( schema , false )
2931 this . handleAdditionalPropertiesUndefined ( schema )
3032 this . convertNullTypeToNullValue ( schema )
3133 this . handleOneOfConst ( schema , schemaName )
@@ -36,11 +38,14 @@ export class SchemaModifier {
3638 const visit = new Set ( ) ;
3739 traverse ( this . root , {
3840 onSchemaProperty : ( schema ) => {
39- this . simplifySingleMapSchema ( schema , visit )
41+ this . simplifySingleMapSchema ( schema , visit ) ;
42+ this . handleAdditionalPropertiesUndefined ( schema )
43+
4044 } ,
4145 onSchema : ( schema ) => {
4246 if ( ! schema || isReferenceObject ( schema ) ) return ;
4347 this . simplifySingleMapSchema ( schema , visit )
48+ this . handleAdditionalPropertiesUndefined ( schema )
4449 } ,
4550 } ) ;
4651 return this . root
@@ -70,7 +75,6 @@ export class SchemaModifier {
7075 break ;
7176 }
7277 }
73-
7478 // if found string+const, collect all values
7579 if ( hasStringWithConst ) {
7680 for ( const item of schema . oneOf ) {
@@ -84,7 +88,6 @@ export class SchemaModifier {
8488 }
8589 }
8690 }
87-
8891 // Convert to enum
8992 delete schema . oneOf ;
9093 schema . type = 'string' ;
@@ -343,4 +346,95 @@ export class SchemaModifier {
343346 ( schema as any ) . type = 'NullValue' ;
344347 }
345348 }
349+
350+ /**
351+ * Converts additionalProperties with a title into a named property.
352+ *
353+ * @param schema - The schema to process
354+ * @param isNestedProperty - If true, skip conversion (nested properties already have context)
355+ *
356+ * Example:
357+ * Input:
358+ * {
359+ * type: "object",
360+ * properties: { distance: { type: "string" } },
361+ * propertyNames: { title: "field", type: "string" },
362+ * additionalProperties: {
363+ * title: "location",
364+ * $ref: "#/components/schemas/GeoLocation"
365+ * },
366+ * minProperties: 2
367+ * }
368+ *
369+ * Output:
370+ * {
371+ * type: "object",
372+ * properties: {
373+ * distance: { type: "string" },
374+ * location: {
375+ * type: "object",
376+ * additionalProperties: {
377+ * $ref: "#/components/schemas/GeoLocation"
378+ * }
379+ * }
380+ * },
381+ * minProperties: 2
382+ * }
383+ **/
384+ convertAdditionalPropertiesToProperty ( schema : OpenAPIV3 . SchemaObject , isNestedProperty : boolean = false ) : void {
385+ if ( ! schema . additionalProperties || typeof schema . additionalProperties !== 'object' ) {
386+ return ;
387+ }
388+
389+ const additionalProps = schema . additionalProperties as any ;
390+
391+ if ( isNestedProperty ) {
392+ return ;
393+ }
394+
395+ if ( ! additionalProps . title || typeof additionalProps . title !== 'string' ) {
396+ return ;
397+ }
398+
399+ const propertyName = additionalProps . title ;
400+
401+ if ( ! schema . properties ) {
402+ schema . properties = { } ;
403+ }
404+
405+ if ( schema . properties [ propertyName ] ) {
406+ this . logger . warn ( `Property '${ propertyName } ' already exists in schema, skipping additionalProperties conversion` ) ;
407+ return ;
408+ }
409+
410+ const innerAdditionalProps : any = { } ;
411+ for ( const key in additionalProps ) {
412+ if ( key !== 'title' ) {
413+ innerAdditionalProps [ key ] = additionalProps [ key ] ;
414+ }
415+ }
416+ const hasSchemaDefinition = Boolean (
417+ innerAdditionalProps . type ||
418+ innerAdditionalProps . $ref ||
419+ innerAdditionalProps . properties ||
420+ innerAdditionalProps . enum ||
421+ innerAdditionalProps . items ||
422+ innerAdditionalProps . allOf ||
423+ innerAdditionalProps . anyOf ||
424+ innerAdditionalProps . oneOf
425+ ) ;
426+
427+ schema . properties [ propertyName ] = {
428+ type : 'object' ,
429+ additionalProperties : hasSchemaDefinition ? innerAdditionalProps : true
430+ } ;
431+
432+ delete schema . additionalProperties ;
433+
434+ if ( 'propertyNames' in schema ) {
435+ delete schema . propertyNames ;
436+ }
437+
438+ this . logger . info ( `Converted additionalProperties to named property '${ propertyName } ' with type: object` ) ;
439+ }
346440}
0 commit comments