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

Add logic to skip discriminator values for intermediate schemas in multi-level inheritance

Prevent adding discriminator values to schemas that are extended by other schemas in the same discriminator mapping, fixing the never type issue in nested allOf hierarchies

Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com>

+175 -103
+86 -40
packages/openapi-ts/src/openApi/3.0.x/parser/schema.ts
··· 79 79 return discriminators; 80 80 }; 81 81 82 + /** 83 + * Checks if a schema is extended by other schemas in the given discriminator mapping. 84 + * This is used to determine if a schema is a leaf node in the inheritance hierarchy. 85 + */ 86 + const isSchemaExtendedInMapping = ({ 87 + context, 88 + discriminator, 89 + schemaRef, 90 + }: { 91 + context: Context; 92 + discriminator: NonNullable<SchemaObject['discriminator']>; 93 + schemaRef: string; 94 + }): boolean => { 95 + // Check each schema in the discriminator mapping 96 + for (const mappedSchemaRef of Object.values(discriminator.mapping || {})) { 97 + // Skip if it's the same schema 98 + if (mappedSchemaRef === schemaRef) { 99 + continue; 100 + } 101 + 102 + // Resolve the mapped schema 103 + const mappedSchema = context.resolveRef<SchemaObject>(mappedSchemaRef); 104 + 105 + // Check if the mapped schema extends our schema via allOf 106 + if (mappedSchema.allOf) { 107 + for (const item of mappedSchema.allOf) { 108 + if ('$ref' in item && item.$ref === schemaRef) { 109 + return true; 110 + } 111 + } 112 + } 113 + } 114 + 115 + return false; 116 + }; 117 + 82 118 const parseSchemaJsDoc = ({ 83 119 irSchema, 84 120 schema, ··· 419 455 ); 420 456 421 457 if (values.length > 0) { 422 - const valueSchemas: ReadonlyArray<IR.SchemaObject> = values.map( 423 - (value) => ({ 424 - const: value, 425 - type: 'string', 426 - }), 427 - ); 428 - const irDiscriminatorSchema: IR.SchemaObject = { 429 - properties: { 430 - [discriminator.propertyName]: 431 - valueSchemas.length > 1 432 - ? { 433 - items: valueSchemas, 434 - logicalOperator: 'or', 435 - } 436 - : valueSchemas[0]!, 437 - }, 438 - type: 'object', 439 - }; 458 + // Check if the current schema is extended by other schemas in this discriminator mapping 459 + // If so, don't add the discriminator value to avoid conflicts in multi-level inheritance 460 + const isExtended = isSchemaExtendedInMapping({ 461 + context, 462 + discriminator, 463 + schemaRef: state.$ref, 464 + }); 440 465 441 - // Check if the discriminator property is required in any of the discriminator schemas 442 - // by looking at all discriminators with the same property name 443 - const isRequired = discriminators.some( 444 - (d) => 445 - d.discriminator.propertyName === discriminator.propertyName && 446 - // Check in the ref's required array or in the allOf components 447 - (ref.required?.includes(d.discriminator.propertyName) || 448 - (ref.allOf && 449 - ref.allOf.some((item) => { 450 - const resolvedItem = 451 - '$ref' in item 452 - ? context.resolveRef<SchemaObject>(item.$ref) 453 - : item; 454 - return resolvedItem.required?.includes( 455 - d.discriminator.propertyName, 456 - ); 457 - }))), 458 - ); 466 + if (!isExtended) { 467 + const valueSchemas: ReadonlyArray<IR.SchemaObject> = values.map( 468 + (value) => ({ 469 + const: value, 470 + type: 'string', 471 + }), 472 + ); 473 + const irDiscriminatorSchema: IR.SchemaObject = { 474 + properties: { 475 + [discriminator.propertyName]: 476 + valueSchemas.length > 1 477 + ? { 478 + items: valueSchemas, 479 + logicalOperator: 'or', 480 + } 481 + : valueSchemas[0]!, 482 + }, 483 + type: 'object', 484 + }; 459 485 460 - if (isRequired) { 461 - irDiscriminatorSchema.required = [discriminator.propertyName]; 486 + // Check if the discriminator property is required in any of the discriminator schemas 487 + // by looking at all discriminators with the same property name 488 + const isRequired = discriminators.some( 489 + (d) => 490 + d.discriminator.propertyName === discriminator.propertyName && 491 + // Check in the ref's required array or in the allOf components 492 + (ref.required?.includes(d.discriminator.propertyName) || 493 + (ref.allOf && 494 + ref.allOf.some((item) => { 495 + const resolvedItem = 496 + '$ref' in item 497 + ? context.resolveRef<SchemaObject>(item.$ref) 498 + : item; 499 + return resolvedItem.required?.includes( 500 + d.discriminator.propertyName, 501 + ); 502 + }))), 503 + ); 504 + 505 + if (isRequired) { 506 + irDiscriminatorSchema.required = [discriminator.propertyName]; 507 + } 508 + schemaItems.push(irDiscriminatorSchema); 509 + addedDiscriminators.add(discriminator.propertyName); 462 510 } 463 - schemaItems.push(irDiscriminatorSchema); 464 - addedDiscriminators.add(discriminator.propertyName); 465 511 } 466 512 } 467 513 }
+85 -39
packages/openapi-ts/src/openApi/3.1.x/parser/schema.ts
··· 83 83 return discriminators; 84 84 }; 85 85 86 + /** 87 + * Checks if a schema is extended by other schemas in the given discriminator mapping. 88 + * This is used to determine if a schema is a leaf node in the inheritance hierarchy. 89 + */ 90 + const isSchemaExtendedInMapping = ({ 91 + context, 92 + discriminator, 93 + schemaRef, 94 + }: { 95 + context: Context; 96 + discriminator: NonNullable<SchemaObject['discriminator']>; 97 + schemaRef: string; 98 + }): boolean => { 99 + // Check each schema in the discriminator mapping 100 + for (const mappedSchemaRef of Object.values(discriminator.mapping || {})) { 101 + // Skip if it's the same schema 102 + if (mappedSchemaRef === schemaRef) { 103 + continue; 104 + } 105 + 106 + // Resolve the mapped schema 107 + const mappedSchema = context.resolveRef<SchemaObject>(mappedSchemaRef); 108 + 109 + // Check if the mapped schema extends our schema via allOf 110 + if (mappedSchema.allOf) { 111 + for (const item of mappedSchema.allOf) { 112 + if (item.$ref && item.$ref === schemaRef) { 113 + return true; 114 + } 115 + } 116 + } 117 + } 118 + 119 + return false; 120 + }; 121 + 86 122 const parseSchemaJsDoc = ({ 87 123 irSchema, 88 124 schema, ··· 501 537 ); 502 538 503 539 if (values.length > 0) { 504 - const valueSchemas: ReadonlyArray<IR.SchemaObject> = values.map( 505 - (value) => ({ 506 - const: value, 507 - type: 'string', 508 - }), 509 - ); 510 - const irDiscriminatorSchema: IR.SchemaObject = { 511 - properties: { 512 - [discriminator.propertyName]: 513 - valueSchemas.length > 1 514 - ? { 515 - items: valueSchemas, 516 - logicalOperator: 'or', 517 - } 518 - : valueSchemas[0]!, 519 - }, 520 - type: 'object', 521 - }; 540 + // Check if the current schema is extended by other schemas in this discriminator mapping 541 + // If so, don't add the discriminator value to avoid conflicts in multi-level inheritance 542 + const isExtended = isSchemaExtendedInMapping({ 543 + context, 544 + discriminator, 545 + schemaRef: state.$ref, 546 + }); 522 547 523 - // Check if the discriminator property is required in any of the discriminator schemas 524 - // by looking at all discriminators with the same property name 525 - const isRequired = discriminators.some( 526 - (d) => 527 - d.discriminator.propertyName === discriminator.propertyName && 528 - // Check in the ref's required array or in the allOf components 529 - (ref.required?.includes(d.discriminator.propertyName) || 530 - (ref.allOf && 531 - ref.allOf.some((item) => { 532 - const resolvedItem = item.$ref 533 - ? context.resolveRef<SchemaObject>(item.$ref) 534 - : item; 535 - return resolvedItem.required?.includes( 536 - d.discriminator.propertyName, 537 - ); 538 - }))), 539 - ); 548 + if (!isExtended) { 549 + const valueSchemas: ReadonlyArray<IR.SchemaObject> = values.map( 550 + (value) => ({ 551 + const: value, 552 + type: 'string', 553 + }), 554 + ); 555 + const irDiscriminatorSchema: IR.SchemaObject = { 556 + properties: { 557 + [discriminator.propertyName]: 558 + valueSchemas.length > 1 559 + ? { 560 + items: valueSchemas, 561 + logicalOperator: 'or', 562 + } 563 + : valueSchemas[0]!, 564 + }, 565 + type: 'object', 566 + }; 540 567 541 - if (isRequired) { 542 - irDiscriminatorSchema.required = [discriminator.propertyName]; 568 + // Check if the discriminator property is required in any of the discriminator schemas 569 + // by looking at all discriminators with the same property name 570 + const isRequired = discriminators.some( 571 + (d) => 572 + d.discriminator.propertyName === discriminator.propertyName && 573 + // Check in the ref's required array or in the allOf components 574 + (ref.required?.includes(d.discriminator.propertyName) || 575 + (ref.allOf && 576 + ref.allOf.some((item) => { 577 + const resolvedItem = item.$ref 578 + ? context.resolveRef<SchemaObject>(item.$ref) 579 + : item; 580 + return resolvedItem.required?.includes( 581 + d.discriminator.propertyName, 582 + ); 583 + }))), 584 + ); 585 + 586 + if (isRequired) { 587 + irDiscriminatorSchema.required = [discriminator.propertyName]; 588 + } 589 + schemaItems.push(irDiscriminatorSchema); 590 + addedDiscriminators.add(discriminator.propertyName); 543 591 } 544 - schemaItems.push(irDiscriminatorSchema); 545 - addedDiscriminators.add(discriminator.propertyName); 546 592 } 547 593 } 548 594 }
+2 -12
specs/3.0.x/discriminator-allof-nested.json
··· 56 56 { "$ref": "#/components/schemas/VehicleDto" }, 57 57 { 58 58 "type": "object", 59 - "required": ["$type", "modelName"], 59 + "required": ["modelName"], 60 60 "properties": { 61 - "$type": { 62 - "type": "string" 63 - }, 64 61 "modelName": { 65 62 "type": "string" 66 63 } 67 - }, 68 - "discriminator": { 69 - "propertyName": "$type", 70 - "mapping": { 71 - "Car": "#/components/schemas/CarDto", 72 - "Volvo": "#/components/schemas/VolvoDto" 73 - } 74 64 } 75 65 } 76 66 ] ··· 80 70 { "$ref": "#/components/schemas/CarDto" }, 81 71 { 82 72 "type": "object", 83 - "required": ["$type", "seatbeltCount"], 73 + "required": ["seatbeltCount"], 84 74 "properties": { 85 75 "seatbeltCount": { 86 76 "type": "integer"
+2 -12
specs/3.1.x/discriminator-allof-nested.json
··· 56 56 { "$ref": "#/components/schemas/VehicleDto" }, 57 57 { 58 58 "type": "object", 59 - "required": ["$type", "modelName"], 59 + "required": ["modelName"], 60 60 "properties": { 61 - "$type": { 62 - "type": "string" 63 - }, 64 61 "modelName": { 65 62 "type": "string" 66 63 } 67 - }, 68 - "discriminator": { 69 - "propertyName": "$type", 70 - "mapping": { 71 - "Car": "#/components/schemas/CarDto", 72 - "Volvo": "#/components/schemas/VolvoDto" 73 - } 74 64 } 75 65 } 76 66 ] ··· 80 70 { "$ref": "#/components/schemas/CarDto" }, 81 71 { 82 72 "type": "object", 83 - "required": ["$type", "seatbeltCount"], 73 + "required": ["seatbeltCount"], 84 74 "properties": { 85 75 "seatbeltCount": { 86 76 "type": "integer"