fork of hey-api/openapi-ts because I need some additional things
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #3117 from hey-api/copilot/fix-discriminator-in-allof

Fix discriminator resolution in nested allOf inheritance hierarchies

authored by

Lubos and committed by
GitHub
e6a4e483 f842dcd9

+725 -92
+5
.changeset/proud-berries-matter.md
··· 1 + --- 2 + "@hey-api/openapi-ts": patch 3 + --- 4 + 5 + **parser**: improve discriminator support in nested `allOf` fields
+7
packages/openapi-ts-tests/main/test/3.0.x.test.ts
··· 210 210 }, 211 211 { 212 212 config: createConfig({ 213 + input: 'discriminator-allof-nested.json', 214 + output: 'discriminator-allof-nested', 215 + }), 216 + description: 'handles nested allOf with discriminators', 217 + }, 218 + { 219 + config: createConfig({ 213 220 input: 'enum-escape.json', 214 221 output: 'enum-escape', 215 222 }),
+7
packages/openapi-ts-tests/main/test/3.1.x.test.ts
··· 241 241 }, 242 242 { 243 243 config: createConfig({ 244 + input: 'discriminator-allof-nested.json', 245 + output: 'discriminator-allof-nested', 246 + }), 247 + description: 'handles nested allOf with discriminators', 248 + }, 249 + { 250 + config: createConfig({ 244 251 input: 'duplicate-null.json', 245 252 output: 'duplicate-null', 246 253 }),
+6 -12
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/discriminator-all-of/types.gen.ts
··· 9 9 }; 10 10 11 11 export type Bar = Foo & { 12 + bar?: string; 12 13 id: 'Bar'; 13 - } & { 14 - bar?: string; 15 14 }; 16 15 17 16 export type Baz = Foo & { 17 + baz?: string; 18 18 id: 'Baz'; 19 - } & { 20 - baz?: string; 21 19 }; 22 20 23 21 export type Qux = Foo & { 24 - id: 'Qux'; 25 - } & { 26 22 qux?: boolean; 23 + id: 'Qux'; 27 24 }; 28 25 29 26 export type FooMapped = { ··· 31 28 }; 32 29 33 30 export type BarMapped = FooMapped & { 31 + bar?: string; 34 32 id: 'bar'; 35 - } & { 36 - bar?: string; 37 33 }; 38 34 39 35 export type BazMapped = FooMapped & { 36 + baz?: string; 40 37 id: 'baz'; 41 - } & { 42 - baz?: string; 43 38 }; 44 39 45 40 export type QuxMapped = FooMapped & { 46 - id: 'QuxMapped'; 47 - } & { 48 41 qux?: boolean; 42 + id: 'QuxMapped'; 49 43 }; 50 44 51 45 export type FooUnion = ({
+3
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/discriminator-allof-nested/index.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type { CarDto, ClientOptions, GetCarsData, GetCarsResponse, GetCarsResponses, VehicleDto, VolvoDto } from './types.gen';
+36
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/discriminator-allof-nested/types.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type ClientOptions = { 4 + baseUrl: `${string}://${string}` | (string & {}); 5 + }; 6 + 7 + export type VehicleDto = { 8 + $type: string; 9 + id: number; 10 + }; 11 + 12 + export type CarDto = VehicleDto & { 13 + modelName: string; 14 + $type: 'Car' | 'Volvo'; 15 + }; 16 + 17 + export type VolvoDto = CarDto & { 18 + seatbeltCount: number; 19 + $type: 'Volvo'; 20 + }; 21 + 22 + export type GetCarsData = { 23 + body?: never; 24 + path?: never; 25 + query?: never; 26 + url: '/cars'; 27 + }; 28 + 29 + export type GetCarsResponses = { 30 + /** 31 + * List of cars 32 + */ 33 + 200: Array<CarDto | VolvoDto>; 34 + }; 35 + 36 + export type GetCarsResponse = GetCarsResponses[keyof GetCarsResponses];
+1 -2
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-all-of/types.gen.ts
··· 14 14 }; 15 15 16 16 export type Baz = Qux & { 17 - id: 'Baz'; 18 - } & { 19 17 foo: number; 20 18 bar: Date; 21 19 baz: 'foo' | 'bar' | 'baz'; 22 20 qux: number; 21 + id: 'Baz'; 23 22 }; 24 23 25 24 export type Qux = {
+6 -12
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/discriminator-all-of/types.gen.ts
··· 9 9 }; 10 10 11 11 export type Bar = Foo & { 12 + bar?: string; 12 13 id: 'Bar'; 13 - } & { 14 - bar?: string; 15 14 }; 16 15 17 16 export type Baz = Foo & { 17 + baz?: string; 18 18 id: 'Baz'; 19 - } & { 20 - baz?: string; 21 19 }; 22 20 23 21 export type Qux = Foo & { 24 - id: 'Qux'; 25 - } & { 26 22 qux?: boolean; 23 + id: 'Qux'; 27 24 }; 28 25 29 26 export type FooMapped = { ··· 31 28 }; 32 29 33 30 export type BarMapped = FooMapped & { 31 + bar?: string; 34 32 id: 'bar'; 35 - } & { 36 - bar?: string; 37 33 }; 38 34 39 35 export type BazMapped = FooMapped & { 36 + baz?: string; 40 37 id: 'baz'; 41 - } & { 42 - baz?: string; 43 38 }; 44 39 45 40 export type QuxMapped = FooMapped & { 46 - id: 'QuxMapped'; 47 - } & { 48 41 qux?: boolean; 42 + id: 'QuxMapped'; 49 43 }; 50 44 51 45 export type FooUnion = ({
+3
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/discriminator-allof-nested/index.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type { CarDto, ClientOptions, GetCarsData, GetCarsResponse, GetCarsResponses, VehicleDto, VolvoDto } from './types.gen';
+36
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/discriminator-allof-nested/types.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type ClientOptions = { 4 + baseUrl: `${string}://${string}` | (string & {}); 5 + }; 6 + 7 + export type VehicleDto = { 8 + $type: string; 9 + id: number; 10 + }; 11 + 12 + export type CarDto = VehicleDto & { 13 + modelName: string; 14 + $type: 'Car' | 'Volvo'; 15 + }; 16 + 17 + export type VolvoDto = CarDto & { 18 + seatbeltCount: number; 19 + $type: 'Volvo'; 20 + }; 21 + 22 + export type GetCarsData = { 23 + body?: never; 24 + path?: never; 25 + query?: never; 26 + url: '/cars'; 27 + }; 28 + 29 + export type GetCarsResponses = { 30 + /** 31 + * List of cars 32 + */ 33 + 200: Array<CarDto | VolvoDto>; 34 + }; 35 + 36 + export type GetCarsResponse = GetCarsResponses[keyof GetCarsResponses];
+1 -2
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-all-of/types.gen.ts
··· 14 14 }; 15 15 16 16 export type Baz = Qux & { 17 - id: 'Baz'; 18 - } & { 19 17 foo: number; 20 18 bar: Date; 21 19 baz: 'foo' | 'bar' | 'baz'; 22 20 qux: number; 21 + id: 'Baz'; 23 22 }; 24 23 25 24 export type Qux = {
+223 -32
packages/openapi-ts/src/openApi/3.0.x/parser/schema.ts
··· 28 28 return; 29 29 }; 30 30 31 + /** 32 + * Recursively finds discriminators in a schema, including nested allOf compositions. 33 + * This is needed when a schema extends another schema via allOf, and that parent 34 + * schema is itself an allOf composition with discriminators in inline schemas. 35 + */ 36 + const findDiscriminatorsInSchema = ({ 37 + context, 38 + discriminators = [], 39 + schema, 40 + }: { 41 + context: Context; 42 + discriminators?: Array<{ 43 + discriminator: NonNullable<SchemaObject['discriminator']>; 44 + oneOf?: SchemaObject['oneOf']; 45 + }>; 46 + schema: SchemaObject; 47 + }): Array<{ 48 + discriminator: NonNullable<SchemaObject['discriminator']>; 49 + oneOf?: SchemaObject['oneOf']; 50 + }> => { 51 + // Check if this schema has a discriminator 52 + if (schema.discriminator) { 53 + discriminators.push({ 54 + discriminator: schema.discriminator, 55 + oneOf: schema.oneOf, 56 + }); 57 + } 58 + 59 + // If this schema is an allOf composition, recursively search in its components 60 + if (schema.allOf) { 61 + for (const compositionSchema of schema.allOf) { 62 + let resolvedSchema: SchemaObject; 63 + if ('$ref' in compositionSchema) { 64 + resolvedSchema = context.resolveRef<SchemaObject>( 65 + compositionSchema.$ref, 66 + ); 67 + } else { 68 + resolvedSchema = compositionSchema; 69 + } 70 + 71 + findDiscriminatorsInSchema({ 72 + context, 73 + discriminators, 74 + schema: resolvedSchema, 75 + }); 76 + } 77 + } 78 + 79 + return discriminators; 80 + }; 81 + 82 + /** 83 + * Gets all discriminator values for a schema and its children in the inheritance hierarchy. 84 + * For intermediate schemas (those that are extended by others), returns a union of all values. 85 + */ 86 + const getAllDiscriminatorValues = ({ 87 + context, 88 + discriminator, 89 + schemaRef, 90 + }: { 91 + context: Context; 92 + discriminator: NonNullable<SchemaObject['discriminator']>; 93 + schemaRef: string; 94 + }): Array<string> => { 95 + const values: Array<string> = []; 96 + 97 + // Check each entry in the discriminator mapping 98 + for (const [value, mappedSchemaRef] of Object.entries( 99 + discriminator.mapping || {}, 100 + )) { 101 + if (mappedSchemaRef === schemaRef) { 102 + // This is the current schema's own value 103 + values.push(value); 104 + continue; 105 + } 106 + 107 + // Check if the mapped schema extends the current schema 108 + const mappedSchema = context.resolveRef<SchemaObject>(mappedSchemaRef); 109 + if (mappedSchema.allOf) { 110 + for (const item of mappedSchema.allOf) { 111 + if ('$ref' in item && item.$ref === schemaRef) { 112 + // This schema extends the current schema, add its value 113 + values.push(value); 114 + break; 115 + } 116 + } 117 + } 118 + } 119 + 120 + return values; 121 + }; 122 + 31 123 const parseSchemaJsDoc = ({ 32 124 irSchema, 33 125 schema, ··· 307 399 308 400 const compositionSchemas = schema.allOf; 309 401 402 + // Collect discriminator information to add after all compositions are processed 403 + type DiscriminatorInfo = { 404 + discriminator: NonNullable<SchemaObject['discriminator']>; 405 + isRequired: boolean; 406 + values: ReadonlyArray<string>; 407 + }; 408 + const discriminatorsToAdd: Array<DiscriminatorInfo> = []; 409 + const addedDiscriminators = new Set<string>(); 410 + 310 411 for (const compositionSchema of compositionSchemas) { 311 412 const originalInAllOf = state.inAllOf; 312 413 // Don't propagate inAllOf flag to $ref schemas to avoid issues with reusable components ··· 339 440 if ('$ref' in compositionSchema) { 340 441 const ref = context.resolveRef<SchemaObject>(compositionSchema.$ref); 341 442 // `$ref` should be passed from the root `parseSchema()` call 342 - if (ref.discriminator && state.$ref) { 343 - const values = discriminatorValues( 344 - state.$ref, 345 - ref.discriminator.mapping, 346 - // If the ref has oneOf, we only use the schema name as the value 347 - // only if current schema is part of the oneOf. Else it is extending 348 - // the ref schema 349 - ref.oneOf 350 - ? () => ref.oneOf!.some((o) => '$ref' in o && o.$ref === state.$ref) 351 - : undefined, 352 - ); 443 + if (state.$ref) { 444 + // Find all discriminators in the referenced schema, including nested allOf compositions 445 + const discriminators = findDiscriminatorsInSchema({ 446 + context, 447 + schema: ref, 448 + }); 353 449 354 - if (values.length > 0) { 355 - const valueSchemas: ReadonlyArray<IR.SchemaObject> = values.map( 356 - (value) => ({ 357 - const: value, 358 - type: 'string', 359 - }), 450 + // Process each discriminator found 451 + for (const { discriminator, oneOf } of discriminators) { 452 + // Skip if we've already collected this discriminator property 453 + if (addedDiscriminators.has(discriminator.propertyName)) { 454 + continue; 455 + } 456 + 457 + const values = discriminatorValues( 458 + state.$ref, 459 + discriminator.mapping, 460 + // If the ref has oneOf, we only use the schema name as the value 461 + // only if current schema is part of the oneOf. Else it is extending 462 + // the ref schema 463 + oneOf 464 + ? () => oneOf.some((o) => '$ref' in o && o.$ref === state.$ref) 465 + : undefined, 360 466 ); 361 - const irDiscriminatorSchema: IR.SchemaObject = { 362 - properties: { 363 - [ref.discriminator.propertyName]: 364 - valueSchemas.length > 1 365 - ? { 366 - items: valueSchemas, 367 - logicalOperator: 'or', 368 - } 369 - : valueSchemas[0]!, 370 - }, 371 - type: 'object', 372 - }; 373 - if (ref.required?.includes(ref.discriminator.propertyName)) { 374 - irDiscriminatorSchema.required = [ref.discriminator.propertyName]; 467 + 468 + if (values.length > 0) { 469 + // Check if the discriminator property is required in any of the discriminator schemas 470 + const isRequired = discriminators.some( 471 + (d) => 472 + d.discriminator.propertyName === discriminator.propertyName && 473 + // Check in the ref's required array or in the allOf components 474 + (ref.required?.includes(d.discriminator.propertyName) || 475 + (ref.allOf && 476 + ref.allOf.some((item) => { 477 + const resolvedItem = 478 + '$ref' in item 479 + ? context.resolveRef<SchemaObject>(item.$ref) 480 + : item; 481 + return resolvedItem.required?.includes( 482 + d.discriminator.propertyName, 483 + ); 484 + }))), 485 + ); 486 + 487 + discriminatorsToAdd.push({ 488 + discriminator, 489 + isRequired, 490 + values, 491 + }); 492 + addedDiscriminators.add(discriminator.propertyName); 493 + } 494 + } 495 + } 496 + } 497 + } 498 + 499 + // Now add discriminators after all compositions have been processed 500 + for (const { discriminator, isRequired, values } of discriminatorsToAdd) { 501 + // Get all discriminator values including children for union types 502 + const allValues = getAllDiscriminatorValues({ 503 + context, 504 + discriminator, 505 + schemaRef: state.$ref!, 506 + }); 507 + 508 + // Use allValues if we found children, otherwise use the original values 509 + const finalValues = allValues.length > 0 ? allValues : values; 510 + 511 + const valueSchemas: ReadonlyArray<IR.SchemaObject> = finalValues.map( 512 + (value) => ({ 513 + const: value, 514 + type: 'string', 515 + }), 516 + ); 517 + 518 + const discriminatorProperty: IR.SchemaObject = 519 + valueSchemas.length > 1 520 + ? { 521 + items: valueSchemas, 522 + logicalOperator: 'or', 375 523 } 376 - schemaItems.push(irDiscriminatorSchema); 524 + : valueSchemas[0]!; 525 + 526 + // Find the inline schema (non-$ref) to merge the discriminator property into 527 + // The inline schema should be the last non-$ref item in schemaItems 528 + let inlineSchema: IR.SchemaObject | undefined; 529 + for (let i = schemaItems.length - 1; i >= 0; i--) { 530 + const item = schemaItems[i]!; 531 + // Check if this is not a $ref schema by looking for properties or checking if it came from an inline schema 532 + if (item.type === 'object' || item.properties) { 533 + inlineSchema = item; 534 + break; 535 + } 536 + } 537 + 538 + // If we found an inline schema, add the discriminator property to it 539 + if (inlineSchema) { 540 + if (!inlineSchema.properties) { 541 + inlineSchema.properties = {}; 542 + } 543 + inlineSchema.properties[discriminator.propertyName] = 544 + discriminatorProperty; 545 + 546 + if (isRequired) { 547 + if (!inlineSchema.required) { 548 + inlineSchema.required = []; 549 + } 550 + if (!inlineSchema.required.includes(discriminator.propertyName)) { 551 + inlineSchema.required = [ 552 + ...inlineSchema.required, 553 + discriminator.propertyName, 554 + ]; 377 555 } 378 556 } 557 + } else { 558 + // Fallback: create a separate discriminator schema if no inline schema found 559 + const irDiscriminatorSchema: IR.SchemaObject = { 560 + properties: { 561 + [discriminator.propertyName]: discriminatorProperty, 562 + }, 563 + type: 'object', 564 + }; 565 + 566 + if (isRequired) { 567 + irDiscriminatorSchema.required = [discriminator.propertyName]; 568 + } 569 + schemaItems.push(irDiscriminatorSchema); 379 570 } 380 571 } 381 572
+223 -32
packages/openapi-ts/src/openApi/3.1.x/parser/schema.ts
··· 32 32 return []; 33 33 }; 34 34 35 + /** 36 + * Recursively finds discriminators in a schema, including nested allOf compositions. 37 + * This is needed when a schema extends another schema via allOf, and that parent 38 + * schema is itself an allOf composition with discriminators in inline schemas. 39 + */ 40 + const findDiscriminatorsInSchema = ({ 41 + context, 42 + discriminators = [], 43 + schema, 44 + }: { 45 + context: Context; 46 + discriminators?: Array<{ 47 + discriminator: NonNullable<SchemaObject['discriminator']>; 48 + oneOf?: SchemaObject['oneOf']; 49 + }>; 50 + schema: SchemaObject; 51 + }): Array<{ 52 + discriminator: NonNullable<SchemaObject['discriminator']>; 53 + oneOf?: SchemaObject['oneOf']; 54 + }> => { 55 + // Check if this schema has a discriminator 56 + if (schema.discriminator) { 57 + discriminators.push({ 58 + discriminator: schema.discriminator, 59 + oneOf: schema.oneOf, 60 + }); 61 + } 62 + 63 + // If this schema is an allOf composition, recursively search in its components 64 + if (schema.allOf) { 65 + for (const compositionSchema of schema.allOf) { 66 + let resolvedSchema: SchemaObject; 67 + if (compositionSchema.$ref) { 68 + resolvedSchema = context.resolveRef<SchemaObject>( 69 + compositionSchema.$ref, 70 + ); 71 + } else { 72 + resolvedSchema = compositionSchema; 73 + } 74 + 75 + findDiscriminatorsInSchema({ 76 + context, 77 + discriminators, 78 + schema: resolvedSchema, 79 + }); 80 + } 81 + } 82 + 83 + return discriminators; 84 + }; 85 + 86 + /** 87 + * Gets all discriminator values for a schema and its children in the inheritance hierarchy. 88 + * For intermediate schemas (those that are extended by others), returns a union of all values. 89 + */ 90 + const getAllDiscriminatorValues = ({ 91 + context, 92 + discriminator, 93 + schemaRef, 94 + }: { 95 + context: Context; 96 + discriminator: NonNullable<SchemaObject['discriminator']>; 97 + schemaRef: string; 98 + }): Array<string> => { 99 + const values: Array<string> = []; 100 + 101 + // Check each entry in the discriminator mapping 102 + for (const [value, mappedSchemaRef] of Object.entries( 103 + discriminator.mapping || {}, 104 + )) { 105 + if (mappedSchemaRef === schemaRef) { 106 + // This is the current schema's own value 107 + values.push(value); 108 + continue; 109 + } 110 + 111 + // Check if the mapped schema extends the current schema 112 + const mappedSchema = context.resolveRef<SchemaObject>(mappedSchemaRef); 113 + if (mappedSchema.allOf) { 114 + for (const item of mappedSchema.allOf) { 115 + if (item.$ref && item.$ref === schemaRef) { 116 + // This schema extends the current schema, add its value 117 + values.push(value); 118 + break; 119 + } 120 + } 121 + } 122 + } 123 + 124 + return values; 125 + }; 126 + 35 127 const parseSchemaJsDoc = ({ 36 128 irSchema, 37 129 schema, ··· 389 481 390 482 const compositionSchemas = schema.allOf; 391 483 484 + // Collect discriminator information to add after all compositions are processed 485 + type DiscriminatorInfo = { 486 + discriminator: NonNullable<SchemaObject['discriminator']>; 487 + isRequired: boolean; 488 + values: ReadonlyArray<string>; 489 + }; 490 + const discriminatorsToAdd: Array<DiscriminatorInfo> = []; 491 + const addedDiscriminators = new Set<string>(); 492 + 392 493 for (const compositionSchema of compositionSchemas) { 393 494 const originalInAllOf = state.inAllOf; 394 495 // Don't propagate inAllOf flag to $ref schemas to avoid issues with reusable components ··· 421 522 if (compositionSchema.$ref) { 422 523 const ref = context.resolveRef<SchemaObject>(compositionSchema.$ref); 423 524 // `$ref` should be passed from the root `parseSchema()` call 424 - if (ref.discriminator && state.$ref) { 425 - const values = discriminatorValues( 426 - state.$ref, 427 - ref.discriminator.mapping, 428 - // If the ref has oneOf, we only use the schema name as the value 429 - // only if current schema is part of the oneOf. Else it is extending 430 - // the ref schema 431 - ref.oneOf 432 - ? () => ref.oneOf!.some((o) => '$ref' in o && o.$ref === state.$ref) 433 - : undefined, 434 - ); 435 - if (values.length > 0) { 436 - const valueSchemas: ReadonlyArray<IR.SchemaObject> = values.map( 437 - (value) => ({ 438 - const: value, 439 - type: 'string', 440 - }), 525 + if (state.$ref) { 526 + // Find all discriminators in the referenced schema, including nested allOf compositions 527 + const discriminators = findDiscriminatorsInSchema({ 528 + context, 529 + schema: ref, 530 + }); 531 + 532 + // Process each discriminator found 533 + for (const { discriminator, oneOf } of discriminators) { 534 + // Skip if we've already collected this discriminator property 535 + if (addedDiscriminators.has(discriminator.propertyName)) { 536 + continue; 537 + } 538 + 539 + const values = discriminatorValues( 540 + state.$ref, 541 + discriminator.mapping, 542 + // If the ref has oneOf, we only use the schema name as the value 543 + // only if current schema is part of the oneOf. Else it is extending 544 + // the ref schema 545 + oneOf 546 + ? () => oneOf.some((o) => '$ref' in o && o.$ref === state.$ref) 547 + : undefined, 441 548 ); 442 - const irDiscriminatorSchema: IR.SchemaObject = { 443 - properties: { 444 - [ref.discriminator.propertyName]: 445 - valueSchemas.length > 1 446 - ? { 447 - items: valueSchemas, 448 - logicalOperator: 'or', 449 - } 450 - : valueSchemas[0]!, 451 - }, 452 - type: 'object', 453 - }; 454 - if (ref.required?.includes(ref.discriminator.propertyName)) { 455 - irDiscriminatorSchema.required = [ref.discriminator.propertyName]; 549 + 550 + if (values.length > 0) { 551 + // Check if the discriminator property is required in any of the discriminator schemas 552 + const isRequired = discriminators.some( 553 + (d) => 554 + d.discriminator.propertyName === discriminator.propertyName && 555 + // Check in the ref's required array or in the allOf components 556 + (ref.required?.includes(d.discriminator.propertyName) || 557 + (ref.allOf && 558 + ref.allOf.some((item) => { 559 + const resolvedItem = item.$ref 560 + ? context.resolveRef<SchemaObject>(item.$ref) 561 + : item; 562 + return resolvedItem.required?.includes( 563 + d.discriminator.propertyName, 564 + ); 565 + }))), 566 + ); 567 + 568 + discriminatorsToAdd.push({ 569 + discriminator, 570 + isRequired, 571 + values, 572 + }); 573 + addedDiscriminators.add(discriminator.propertyName); 574 + } 575 + } 576 + } 577 + } 578 + } 579 + 580 + // Now add discriminators after all compositions have been processed 581 + for (const { discriminator, isRequired, values } of discriminatorsToAdd) { 582 + // Get all discriminator values including children for union types 583 + const allValues = getAllDiscriminatorValues({ 584 + context, 585 + discriminator, 586 + schemaRef: state.$ref!, 587 + }); 588 + 589 + // Use allValues if we found children, otherwise use the original values 590 + const finalValues = allValues.length > 0 ? allValues : values; 591 + 592 + const valueSchemas: ReadonlyArray<IR.SchemaObject> = finalValues.map( 593 + (value) => ({ 594 + const: value, 595 + type: 'string', 596 + }), 597 + ); 598 + 599 + const discriminatorProperty: IR.SchemaObject = 600 + valueSchemas.length > 1 601 + ? { 602 + items: valueSchemas, 603 + logicalOperator: 'or', 456 604 } 457 - schemaItems.push(irDiscriminatorSchema); 605 + : valueSchemas[0]!; 606 + 607 + // Find the inline schema (non-$ref) to merge the discriminator property into 608 + // The inline schema should be the last non-$ref item in schemaItems 609 + let inlineSchema: IR.SchemaObject | undefined; 610 + for (let i = schemaItems.length - 1; i >= 0; i--) { 611 + const item = schemaItems[i]!; 612 + // Check if this is not a $ref schema by looking for properties or checking if it came from an inline schema 613 + if (item.type === 'object' || item.properties) { 614 + inlineSchema = item; 615 + break; 616 + } 617 + } 618 + 619 + // If we found an inline schema, add the discriminator property to it 620 + if (inlineSchema) { 621 + if (!inlineSchema.properties) { 622 + inlineSchema.properties = {}; 623 + } 624 + inlineSchema.properties[discriminator.propertyName] = 625 + discriminatorProperty; 626 + 627 + if (isRequired) { 628 + if (!inlineSchema.required) { 629 + inlineSchema.required = []; 630 + } 631 + if (!inlineSchema.required.includes(discriminator.propertyName)) { 632 + inlineSchema.required = [ 633 + ...inlineSchema.required, 634 + discriminator.propertyName, 635 + ]; 458 636 } 459 637 } 638 + } else { 639 + // Fallback: create a separate discriminator schema if no inline schema found 640 + const irDiscriminatorSchema: IR.SchemaObject = { 641 + properties: { 642 + [discriminator.propertyName]: discriminatorProperty, 643 + }, 644 + type: 'object', 645 + }; 646 + 647 + if (isRequired) { 648 + irDiscriminatorSchema.required = [discriminator.propertyName]; 649 + } 650 + schemaItems.push(irDiscriminatorSchema); 460 651 } 461 652 } 462 653
+84
specs/3.0.x/discriminator-allof-nested.json
··· 1 + { 2 + "openapi": "3.0.3", 3 + "info": { 4 + "title": "Minimal Polymorphic Discriminator Reproduction", 5 + "version": "1.0.0", 6 + "description": "Demonstrates an issue where TypeScript type generation results in wrong discriminator for nested allOf inheritance with discriminators at multiple levels." 7 + }, 8 + "paths": { 9 + "/cars": { 10 + "get": { 11 + "summary": "Get cars", 12 + "responses": { 13 + "200": { 14 + "description": "List of cars", 15 + "content": { 16 + "application/json": { 17 + "schema": { 18 + "type": "array", 19 + "items": { 20 + "oneOf": [ 21 + { "$ref": "#/components/schemas/CarDto" }, 22 + { "$ref": "#/components/schemas/VolvoDto" } 23 + ] 24 + } 25 + } 26 + } 27 + } 28 + } 29 + } 30 + } 31 + } 32 + }, 33 + "components": { 34 + "schemas": { 35 + "VehicleDto": { 36 + "type": "object", 37 + "required": ["$type", "id"], 38 + "properties": { 39 + "$type": { 40 + "type": "string" 41 + }, 42 + "id": { 43 + "type": "integer" 44 + } 45 + }, 46 + "discriminator": { 47 + "propertyName": "$type", 48 + "mapping": { 49 + "Car": "#/components/schemas/CarDto", 50 + "Volvo": "#/components/schemas/VolvoDto" 51 + } 52 + } 53 + }, 54 + "CarDto": { 55 + "allOf": [ 56 + { "$ref": "#/components/schemas/VehicleDto" }, 57 + { 58 + "type": "object", 59 + "required": ["modelName"], 60 + "properties": { 61 + "modelName": { 62 + "type": "string" 63 + } 64 + } 65 + } 66 + ] 67 + }, 68 + "VolvoDto": { 69 + "allOf": [ 70 + { "$ref": "#/components/schemas/CarDto" }, 71 + { 72 + "type": "object", 73 + "required": ["seatbeltCount"], 74 + "properties": { 75 + "seatbeltCount": { 76 + "type": "integer" 77 + } 78 + } 79 + } 80 + ] 81 + } 82 + } 83 + } 84 + }
+84
specs/3.1.x/discriminator-allof-nested.json
··· 1 + { 2 + "openapi": "3.1.0", 3 + "info": { 4 + "title": "Minimal Polymorphic Discriminator Reproduction", 5 + "version": "1.0.0", 6 + "description": "Demonstrates an issue where TypeScript type generation results in wrong discriminator for nested allOf inheritance with discriminators at multiple levels." 7 + }, 8 + "paths": { 9 + "/cars": { 10 + "get": { 11 + "summary": "Get cars", 12 + "responses": { 13 + "200": { 14 + "description": "List of cars", 15 + "content": { 16 + "application/json": { 17 + "schema": { 18 + "type": "array", 19 + "items": { 20 + "oneOf": [ 21 + { "$ref": "#/components/schemas/CarDto" }, 22 + { "$ref": "#/components/schemas/VolvoDto" } 23 + ] 24 + } 25 + } 26 + } 27 + } 28 + } 29 + } 30 + } 31 + } 32 + }, 33 + "components": { 34 + "schemas": { 35 + "VehicleDto": { 36 + "type": "object", 37 + "required": ["$type", "id"], 38 + "properties": { 39 + "$type": { 40 + "type": "string" 41 + }, 42 + "id": { 43 + "type": "integer" 44 + } 45 + }, 46 + "discriminator": { 47 + "propertyName": "$type", 48 + "mapping": { 49 + "Car": "#/components/schemas/CarDto", 50 + "Volvo": "#/components/schemas/VolvoDto" 51 + } 52 + } 53 + }, 54 + "CarDto": { 55 + "allOf": [ 56 + { "$ref": "#/components/schemas/VehicleDto" }, 57 + { 58 + "type": "object", 59 + "required": ["modelName"], 60 + "properties": { 61 + "modelName": { 62 + "type": "string" 63 + } 64 + } 65 + } 66 + ] 67 + }, 68 + "VolvoDto": { 69 + "allOf": [ 70 + { "$ref": "#/components/schemas/CarDto" }, 71 + { 72 + "type": "object", 73 + "required": ["seatbeltCount"], 74 + "properties": { 75 + "seatbeltCount": { 76 + "type": "integer" 77 + } 78 + } 79 + } 80 + ] 81 + } 82 + } 83 + } 84 + }