@@ -34,8 +34,9 @@ import type { ReferenceObject, SchemaObject, TransformNodeOptions } from "../typ
3434export default function transformSchemaObject (
3535 schemaObject : SchemaObject | ReferenceObject ,
3636 options : TransformNodeOptions ,
37+ fromAdditionalProperties = false ,
3738) : ts . TypeNode {
38- const type = transformSchemaObjectWithComposition ( schemaObject , options ) ;
39+ const type = transformSchemaObjectWithComposition ( schemaObject , options , fromAdditionalProperties ) ;
3940 if ( typeof options . ctx . postTransform === "function" ) {
4041 const postTransformResult = options . ctx . postTransform ( type , options ) ;
4142 if ( postTransformResult ) {
@@ -51,6 +52,7 @@ export default function transformSchemaObject(
5152export function transformSchemaObjectWithComposition (
5253 schemaObject : SchemaObject | ReferenceObject ,
5354 options : TransformNodeOptions ,
55+ fromAdditionalProperties = false ,
5456) : ts . TypeNode {
5557 /**
5658 * Unexpected types & edge cases
@@ -138,14 +140,39 @@ export function transformSchemaObjectWithComposition(
138140
139141 // hoist array with valid enum values to top level if string/number enum and option is enabled
140142 if ( options . ctx . enumValues && schemaObject . enum . every ( ( v ) => typeof v === "string" || typeof v === "number" ) ) {
141- let enumValuesVariableName = parseRef ( options . path ?? "" ) . pointer . join ( "/" ) ;
143+ const parsed = parseRef ( options . path ?? "" ) ;
144+ let enumValuesVariableName = parsed . pointer . join ( "/" ) ;
142145 // allow #/components/schemas to have simpler names
143146 enumValuesVariableName = enumValuesVariableName . replace ( "components/schemas" , "" ) ;
144147 enumValuesVariableName = `${ enumValuesVariableName } Values` ;
145148
149+ // build a ref path for the type that ignores union indices (anyOf/oneOf) so
150+ // type references remain stable even when names include union positions
151+ const cleanedPointer : string [ ] = [ ] ;
152+ for ( let i = 0 ; i < parsed . pointer . length ; i ++ ) {
153+ // Example: #/paths/analytics/data/get/responses/400/content/application/json/anyOf/0/message
154+ const segment = parsed . pointer [ i ] ;
155+ if ( ( segment === "anyOf" || segment === "oneOf" ) && i < parsed . pointer . length - 1 ) {
156+ const next = parsed . pointer [ i + 1 ] ;
157+ if ( / ^ \d + $ / . test ( next ) ) {
158+ // If we encounter something like "anyOf/0", we want to skip that part of the path
159+ i ++ ;
160+ continue ;
161+ }
162+ }
163+ cleanedPointer . push ( segment ) ;
164+ }
165+ const cleanedRefPath = createRef ( cleanedPointer ) ;
166+
146167 const enumValuesArray = tsArrayLiteralExpression (
147168 enumValuesVariableName ,
148- oapiRef ( options . path ?? "" ) ,
169+ // If fromAdditionalProperties is true we are dealing with a record type and we should append [string] to the generated type
170+ fromAdditionalProperties
171+ ? ts . factory . createIndexedAccessTypeNode (
172+ oapiRef ( cleanedRefPath , undefined , true ) ,
173+ ts . factory . createTypeReferenceNode ( ts . factory . createIdentifier ( "string" ) ) ,
174+ )
175+ : oapiRef ( cleanedRefPath , undefined , true ) ,
149176 schemaObject . enum as ( string | number ) [ ] ,
150177 {
151178 export : true ,
@@ -165,10 +192,16 @@ export function transformSchemaObjectWithComposition(
165192 */
166193
167194 /** Collect oneOf/anyOf */
168- function collectUnionCompositions ( items : ( SchemaObject | ReferenceObject ) [ ] ) {
195+ function collectUnionCompositions ( items : ( SchemaObject | ReferenceObject ) [ ] , unionKey : "anyOf" | "oneOf" ) {
169196 const output : ts . TypeNode [ ] = [ ] ;
170- for ( const item of items ) {
171- output . push ( transformSchemaObject ( item , options ) ) ;
197+ for ( const [ index , item ] of items . entries ( ) ) {
198+ output . push (
199+ transformSchemaObject ( item , {
200+ ...options ,
201+ // include index in path so generated names from nested enums/enumValues are unique
202+ path : createRef ( [ options . path , unionKey , String ( index ) ] ) ,
203+ } ) ,
204+ ) ;
172205 }
173206
174207 return output ;
@@ -233,7 +266,7 @@ export function transformSchemaObjectWithComposition(
233266 }
234267 // anyOf: union
235268 // (note: this may seem counterintuitive, but as TypeScript’s unions are not true XORs, they mimic behavior closer to anyOf than oneOf)
236- const anyOfType = collectUnionCompositions ( schemaObject . anyOf ?? [ ] ) ;
269+ const anyOfType = collectUnionCompositions ( schemaObject . anyOf ?? [ ] , "anyOf" ) ;
237270 if ( anyOfType . length ) {
238271 finalType = tsUnion ( [ ...( finalType ? [ finalType ] : [ ] ) , ...anyOfType ] ) ;
239272 }
@@ -244,6 +277,7 @@ export function transformSchemaObjectWithComposition(
244277 schemaObject . type === "object" &&
245278 ( schemaObject . enum as ( SchemaObject | ReferenceObject ) [ ] ) ) ||
246279 [ ] ,
280+ "oneOf" ,
247281 ) ;
248282 if ( oneOfType . length ) {
249283 // note: oneOf is the only type that may include primitives
@@ -578,7 +612,7 @@ function transformSchemaObjectCore(schemaObject: SchemaObject, options: Transfor
578612 typeof schemaObject . patternProperties === "object" && Object . keys ( schemaObject . patternProperties ) . length ;
579613 const stringIndexTypes = [ ] ;
580614 if ( hasExplicitAdditionalProperties ) {
581- stringIndexTypes . push ( transformSchemaObject ( schemaObject . additionalProperties as SchemaObject , options ) ) ;
615+ stringIndexTypes . push ( transformSchemaObject ( schemaObject . additionalProperties as SchemaObject , options , true ) ) ;
582616 }
583617 if ( hasImplicitAdditionalProperties || ( ! schemaObject . additionalProperties && options . ctx . additionalProperties ) ) {
584618 stringIndexTypes . push ( UNKNOWN ) ;
0 commit comments