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 #3490 from pgraug/fix/discriminator-allof-inline-mapping

fix: explicit discriminator mapping wins over fallback in nested `allOf`

authored by

Lubos and committed by
GitHub
92126685 ef871e42

+373 -55
+6
.changeset/curly-wombats-deliver.md
··· 1 + --- 2 + "@hey-api/openapi-ts": patch 3 + "@hey-api/shared": patch 4 + --- 5 + 6 + **parser**: fix: explicit discriminator mapping wins over fallback in nested `allOf`
+8
packages/openapi-ts-tests/main/test/3.0.x.test.ts
··· 219 219 }, 220 220 { 221 221 config: createConfig({ 222 + input: 'discriminator-allof-inline.json', 223 + output: 'discriminator-allof-inline', 224 + }), 225 + description: 226 + 'handles allOf where inline schema discriminator mapping should take priority over $ref discriminator fallback', 227 + }, 228 + { 229 + config: createConfig({ 222 230 input: 'discriminator-non-string.yaml', 223 231 output: 'discriminator-non-string', 224 232 }),
+8
packages/openapi-ts-tests/main/test/3.1.x.test.ts
··· 245 245 }, 246 246 { 247 247 config: createConfig({ 248 + input: 'discriminator-allof-inline.json', 249 + output: 'discriminator-allof-inline', 250 + }), 251 + description: 252 + 'handles allOf where inline schema discriminator mapping should take priority over $ref discriminator fallback', 253 + }, 254 + { 255 + config: createConfig({ 248 256 input: 'discriminator-non-string.yaml', 249 257 output: 'discriminator-non-string', 250 258 }),
+3
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/discriminator-allof-inline/index.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type { Bar, Baz, ClientOptions, Foo, GetFoosData, GetFoosResponse, GetFoosResponses, Qux } from './types.gen';
+41
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/discriminator-allof-inline/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 Foo = { 8 + $type: string; 9 + foo?: string; 10 + }; 11 + 12 + export type Bar = Omit<Foo, '$type'> & { 13 + $type: 'FooBar'; 14 + bar?: string; 15 + }; 16 + 17 + export type Baz = Omit<Foo, '$type'> & { 18 + baz?: string; 19 + $type: 'FooBaz'; 20 + }; 21 + 22 + export type Qux = Omit<Bar, '$type'> & { 23 + qux?: string; 24 + $type: 'BarQux'; 25 + }; 26 + 27 + export type GetFoosData = { 28 + body?: never; 29 + path?: never; 30 + query?: never; 31 + url: '/foos'; 32 + }; 33 + 34 + export type GetFoosResponses = { 35 + /** 36 + * OK 37 + */ 38 + 200: Bar | Baz | Qux; 39 + }; 40 + 41 + export type GetFoosResponse = GetFoosResponses[keyof GetFoosResponses];
+3
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/discriminator-allof-inline/index.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type { Bar, Baz, ClientOptions, Foo, GetFoosData, GetFoosResponse, GetFoosResponses, Qux } from './types.gen';
+41
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/discriminator-allof-inline/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 Foo = { 8 + $type: string; 9 + foo?: string; 10 + }; 11 + 12 + export type Bar = Omit<Foo, '$type'> & { 13 + $type: 'FooBar'; 14 + bar?: string; 15 + }; 16 + 17 + export type Baz = Omit<Foo, '$type'> & { 18 + baz?: string; 19 + $type: 'FooBaz'; 20 + }; 21 + 22 + export type Qux = Omit<Bar, '$type'> & { 23 + qux?: string; 24 + $type: 'BarQux'; 25 + }; 26 + 27 + export type GetFoosData = { 28 + body?: never; 29 + path?: never; 30 + query?: never; 31 + url: '/foos'; 32 + }; 33 + 34 + export type GetFoosResponses = { 35 + /** 36 + * OK 37 + */ 38 + 200: Bar | Baz | Qux; 39 + }; 40 + 41 + export type GetFoosResponse = GetFoosResponses[keyof GetFoosResponses];
+41 -27
packages/shared/src/openApi/3.0.x/parser/schema.ts
··· 436 436 // Collect discriminator information to add after all compositions are processed 437 437 type DiscriminatorInfo = { 438 438 discriminator: NonNullable<SchemaObject['discriminator']>; 439 + isExplicitMapping: boolean; 439 440 isRequired: boolean; 440 441 values: ReadonlyArray<string>; 441 442 }; 442 443 const discriminatorsToAdd: Array<DiscriminatorInfo> = []; 443 - const addedDiscriminators = new Set<string>(); 444 444 445 445 for (const compositionSchema of compositionSchemas) { 446 446 const originalInAllOf = state.inAllOf; ··· 478 478 schema: ref, 479 479 }); 480 480 481 - // Process each discriminator found 482 481 for (const { discriminator, oneOf } of discriminators) { 483 - // Skip if we've already collected this discriminator property 484 - if (addedDiscriminators.has(discriminator.propertyName)) { 485 - continue; 486 - } 487 - 488 482 const values = discriminatorValues( 489 483 state.$ref, 490 484 discriminator.mapping, ··· 494 488 oneOf ? () => oneOf.some((o) => '$ref' in o && o.$ref === state.$ref) : undefined, 495 489 ); 496 490 497 - if (values.length > 0) { 498 - // Check if the discriminator property is required in any of the discriminator schemas 499 - const isRequired = discriminators.some( 500 - (d) => 501 - d.discriminator.propertyName === discriminator.propertyName && 502 - // Check in the ref's required array or in the allOf components 503 - (ref.required?.includes(d.discriminator.propertyName) || 504 - (ref.allOf && 505 - ref.allOf.some((item) => { 506 - const resolvedItem = 507 - '$ref' in item ? context.resolveRef<SchemaObject>(item.$ref) : item; 508 - return resolvedItem.required?.includes(d.discriminator.propertyName); 509 - }))), 510 - ); 491 + if (values.length === 0) { 492 + continue; 493 + } 511 494 512 - discriminatorsToAdd.push({ 513 - discriminator, 514 - isRequired, 515 - values, 516 - }); 517 - addedDiscriminators.add(discriminator.propertyName); 495 + // True when state.$ref appears directly in the mapping; false when the 496 + // value fell back to the schema name because no mapping entry matched. 497 + const isExplicitMapping = 498 + discriminator.mapping !== undefined && 499 + Object.values(discriminator.mapping).includes(state.$ref); 500 + 501 + // An explicit mapping always beats a same-property fallback collected 502 + // earlier (e.g. from a grandparent discriminator that doesn't list this 503 + // schema). Replace it; otherwise skip the duplicate. 504 + const existingIndex = discriminatorsToAdd.findIndex( 505 + (d) => d.discriminator.propertyName === discriminator.propertyName, 506 + ); 507 + if (existingIndex !== -1) { 508 + if (isExplicitMapping && !discriminatorsToAdd[existingIndex]!.isExplicitMapping) { 509 + discriminatorsToAdd.splice(existingIndex, 1); 510 + } else { 511 + continue; 512 + } 518 513 } 514 + 515 + const isRequired = discriminators.some( 516 + (d) => 517 + d.discriminator.propertyName === discriminator.propertyName && 518 + (ref.required?.includes(d.discriminator.propertyName) || 519 + (ref.allOf && 520 + ref.allOf.some((item) => { 521 + const resolvedItem = 522 + '$ref' in item ? context.resolveRef<SchemaObject>(item.$ref) : item; 523 + return resolvedItem.required?.includes(d.discriminator.propertyName); 524 + }))), 525 + ); 526 + 527 + discriminatorsToAdd.push({ 528 + discriminator, 529 + isExplicitMapping, 530 + isRequired, 531 + values, 532 + }); 519 533 } 520 534 } 521 535 }
+42 -28
packages/shared/src/openApi/3.1.x/parser/schema.ts
··· 531 531 // Collect discriminator information to add after all compositions are processed 532 532 type DiscriminatorInfo = { 533 533 discriminator: NonNullable<SchemaObject['discriminator']>; 534 + isExplicitMapping: boolean; 534 535 isRequired: boolean; 535 536 values: ReadonlyArray<string>; 536 537 }; 537 538 const discriminatorsToAdd: Array<DiscriminatorInfo> = []; 538 - const addedDiscriminators = new Set<string>(); 539 539 540 540 for (const compositionSchema of compositionSchemas) { 541 541 const originalInAllOf = state.inAllOf; ··· 573 573 schema: ref, 574 574 }); 575 575 576 - // Process each discriminator found 577 576 for (const { discriminator, oneOf } of discriminators) { 578 - // Skip if we've already collected this discriminator property 579 - if (addedDiscriminators.has(discriminator.propertyName)) { 580 - continue; 581 - } 582 - 583 577 const values = discriminatorValues( 584 578 state.$ref, 585 579 discriminator.mapping, ··· 589 583 oneOf ? () => oneOf.some((o) => '$ref' in o && o.$ref === state.$ref) : undefined, 590 584 ); 591 585 592 - if (values.length > 0) { 593 - // Check if the discriminator property is required in any of the discriminator schemas 594 - const isRequired = discriminators.some( 595 - (d) => 596 - d.discriminator.propertyName === discriminator.propertyName && 597 - // Check in the ref's required array or in the allOf components 598 - (ref.required?.includes(d.discriminator.propertyName) || 599 - (ref.allOf && 600 - ref.allOf.some((item) => { 601 - const resolvedItem = item.$ref 602 - ? context.resolveRef<SchemaObject>(item.$ref) 603 - : item; 604 - return resolvedItem.required?.includes(d.discriminator.propertyName); 605 - }))), 606 - ); 586 + if (values.length === 0) { 587 + continue; 588 + } 607 589 608 - discriminatorsToAdd.push({ 609 - discriminator, 610 - isRequired, 611 - values, 612 - }); 613 - addedDiscriminators.add(discriminator.propertyName); 590 + // True when state.$ref appears directly in the mapping; false when the 591 + // value fell back to the schema name because no mapping entry matched. 592 + const isExplicitMapping = 593 + discriminator.mapping !== undefined && 594 + Object.values(discriminator.mapping).includes(state.$ref); 595 + 596 + // An explicit mapping always beats a same-property fallback collected 597 + // earlier (e.g. from a grandparent discriminator that doesn't list this 598 + // schema). Replace it; otherwise skip the duplicate. 599 + const existingIndex = discriminatorsToAdd.findIndex( 600 + (d) => d.discriminator.propertyName === discriminator.propertyName, 601 + ); 602 + if (existingIndex !== -1) { 603 + if (isExplicitMapping && !discriminatorsToAdd[existingIndex]!.isExplicitMapping) { 604 + discriminatorsToAdd.splice(existingIndex, 1); 605 + } else { 606 + continue; 607 + } 614 608 } 609 + 610 + const isRequired = discriminators.some( 611 + (d) => 612 + d.discriminator.propertyName === discriminator.propertyName && 613 + (ref.required?.includes(d.discriminator.propertyName) || 614 + (ref.allOf && 615 + ref.allOf.some((item) => { 616 + const resolvedItem = item.$ref 617 + ? context.resolveRef<SchemaObject>(item.$ref) 618 + : item; 619 + return resolvedItem.required?.includes(d.discriminator.propertyName); 620 + }))), 621 + ); 622 + 623 + discriminatorsToAdd.push({ 624 + discriminator, 625 + isExplicitMapping, 626 + isRequired, 627 + values, 628 + }); 615 629 } 616 630 } 617 631 }
+90
specs/3.0.x/discriminator-allof-inline.json
··· 1 + { 2 + "openapi": "3.0.3", 3 + "info": { 4 + "title": "Discriminator allOf inline schema mapping", 5 + "version": "1.0.0", 6 + "description": "Reproduces a bug where a schema extending a parent whose allOf contains both a $ref (with a discriminator that doesn't map the child) and an inline schema (with a discriminator that does map the child) gets the wrong discriminator value. The inline discriminator mapping should win, not the $ref discriminator fallback." 7 + }, 8 + "paths": { 9 + "/foos": { 10 + "get": { 11 + "responses": { 12 + "200": { 13 + "description": "OK", 14 + "content": { 15 + "application/json": { 16 + "schema": { 17 + "oneOf": [ 18 + { "$ref": "#/components/schemas/Bar" }, 19 + { "$ref": "#/components/schemas/Baz" }, 20 + { "$ref": "#/components/schemas/Qux" } 21 + ] 22 + } 23 + } 24 + } 25 + } 26 + } 27 + } 28 + } 29 + }, 30 + "components": { 31 + "schemas": { 32 + "Foo": { 33 + "required": ["$type"], 34 + "type": "object", 35 + "properties": { 36 + "$type": { "type": "string" }, 37 + "foo": { "type": "string" } 38 + }, 39 + "discriminator": { 40 + "propertyName": "$type", 41 + "mapping": { 42 + "FooBar": "#/components/schemas/Bar", 43 + "FooBaz": "#/components/schemas/Baz" 44 + } 45 + } 46 + }, 47 + "Bar": { 48 + "allOf": [ 49 + { "$ref": "#/components/schemas/Foo" }, 50 + { 51 + "required": ["$type"], 52 + "type": "object", 53 + "properties": { 54 + "$type": { "type": "string" }, 55 + "bar": { "type": "string" } 56 + }, 57 + "discriminator": { 58 + "propertyName": "$type", 59 + "mapping": { 60 + "BarQux": "#/components/schemas/Qux" 61 + } 62 + } 63 + } 64 + ] 65 + }, 66 + "Baz": { 67 + "allOf": [ 68 + { "$ref": "#/components/schemas/Foo" }, 69 + { 70 + "type": "object", 71 + "properties": { 72 + "baz": { "type": "string" } 73 + } 74 + } 75 + ] 76 + }, 77 + "Qux": { 78 + "allOf": [ 79 + { "$ref": "#/components/schemas/Bar" }, 80 + { 81 + "type": "object", 82 + "properties": { 83 + "qux": { "type": "string" } 84 + } 85 + } 86 + ] 87 + } 88 + } 89 + } 90 + }
+90
specs/3.1.x/discriminator-allof-inline.json
··· 1 + { 2 + "openapi": "3.1.0", 3 + "info": { 4 + "title": "Discriminator allOf inline schema mapping", 5 + "version": "1.0.0", 6 + "description": "Reproduces a bug where a schema extending a parent whose allOf contains both a $ref (with a discriminator that doesn't map the child) and an inline schema (with a discriminator that does map the child) gets the wrong discriminator value. The inline discriminator mapping should win, not the $ref discriminator fallback." 7 + }, 8 + "paths": { 9 + "/foos": { 10 + "get": { 11 + "responses": { 12 + "200": { 13 + "description": "OK", 14 + "content": { 15 + "application/json": { 16 + "schema": { 17 + "oneOf": [ 18 + { "$ref": "#/components/schemas/Bar" }, 19 + { "$ref": "#/components/schemas/Baz" }, 20 + { "$ref": "#/components/schemas/Qux" } 21 + ] 22 + } 23 + } 24 + } 25 + } 26 + } 27 + } 28 + } 29 + }, 30 + "components": { 31 + "schemas": { 32 + "Foo": { 33 + "required": ["$type"], 34 + "type": "object", 35 + "properties": { 36 + "$type": { "type": "string" }, 37 + "foo": { "type": "string" } 38 + }, 39 + "discriminator": { 40 + "propertyName": "$type", 41 + "mapping": { 42 + "FooBar": "#/components/schemas/Bar", 43 + "FooBaz": "#/components/schemas/Baz" 44 + } 45 + } 46 + }, 47 + "Bar": { 48 + "allOf": [ 49 + { "$ref": "#/components/schemas/Foo" }, 50 + { 51 + "required": ["$type"], 52 + "type": "object", 53 + "properties": { 54 + "$type": { "type": "string" }, 55 + "bar": { "type": "string" } 56 + }, 57 + "discriminator": { 58 + "propertyName": "$type", 59 + "mapping": { 60 + "BarQux": "#/components/schemas/Qux" 61 + } 62 + } 63 + } 64 + ] 65 + }, 66 + "Baz": { 67 + "allOf": [ 68 + { "$ref": "#/components/schemas/Foo" }, 69 + { 70 + "type": "object", 71 + "properties": { 72 + "baz": { "type": "string" } 73 + } 74 + } 75 + ] 76 + }, 77 + "Qux": { 78 + "allOf": [ 79 + { "$ref": "#/components/schemas/Bar" }, 80 + { 81 + "type": "object", 82 + "properties": { 83 + "qux": { "type": "string" } 84 + } 85 + } 86 + ] 87 + } 88 + } 89 + } 90 + }