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.

Fix discriminator union types for intermediate schemas

Changed logic to include all child discriminator values in a union for intermediate schemas (e.g., CarDto now has $type: 'Car' | 'Volvo' instead of no discriminator). Removed unused helper function.

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

+127 -113
+2
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/discriminator-allof-nested/types.gen.ts
··· 10 10 }; 11 11 12 12 export type CarDto = VehicleDto & { 13 + $type: 'Car' | 'Volvo'; 14 + } & { 13 15 modelName: string; 14 16 }; 15 17
+2
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/discriminator-allof-nested/types.gen.ts
··· 10 10 }; 11 11 12 12 export type CarDto = VehicleDto & { 13 + $type: 'Car' | 'Volvo'; 14 + } & { 13 15 modelName: string; 14 16 }; 15 17
+62 -57
packages/openapi-ts/src/openApi/3.0.x/parser/schema.ts
··· 80 80 }; 81 81 82 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. 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 85 */ 86 - const isSchemaExtendedInMapping = ({ 86 + const getAllDiscriminatorValues = ({ 87 87 context, 88 88 discriminator, 89 89 schemaRef, ··· 91 91 context: Context; 92 92 discriminator: NonNullable<SchemaObject['discriminator']>; 93 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 94 + }): string[] => { 95 + const values: string[] = []; 96 + 97 + // Check each entry in the discriminator mapping 98 + for (const [value, mappedSchemaRef] of Object.entries( 99 + discriminator.mapping || {}, 100 + )) { 98 101 if (mappedSchemaRef === schemaRef) { 102 + // This is the current schema's own value 103 + values.push(value); 99 104 continue; 100 105 } 101 106 102 - // Resolve the mapped schema 107 + // Check if the mapped schema extends the current schema 103 108 const mappedSchema = context.resolveRef<SchemaObject>(mappedSchemaRef); 104 - 105 - // Check if the mapped schema extends our schema via allOf 106 109 if (mappedSchema.allOf) { 107 110 for (const item of mappedSchema.allOf) { 108 111 if ('$ref' in item && item.$ref === schemaRef) { 109 - return true; 112 + // This schema extends the current schema, add its value 113 + values.push(value); 114 + break; 110 115 } 111 116 } 112 117 } 113 118 } 114 119 115 - return false; 120 + return values; 116 121 }; 117 122 118 123 const parseSchemaJsDoc = ({ ··· 455 460 ); 456 461 457 462 if (values.length > 0) { 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({ 463 + // For schemas that are extended by others, we need to include all child discriminator values 464 + // to create a union type (e.g., CarDto should have $type: 'Car' | 'Volvo') 465 + const allValues = getAllDiscriminatorValues({ 461 466 context, 462 467 discriminator, 463 468 schemaRef: state.$ref, 464 469 }); 465 470 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 - }; 471 + // Use allValues if we found children, otherwise use the original values 472 + const finalValues = allValues.length > 0 ? allValues : values; 485 473 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 - ); 474 + const valueSchemas: ReadonlyArray<IR.SchemaObject> = 475 + finalValues.map((value) => ({ 476 + const: value, 477 + type: 'string', 478 + })); 479 + const irDiscriminatorSchema: IR.SchemaObject = { 480 + properties: { 481 + [discriminator.propertyName]: 482 + valueSchemas.length > 1 483 + ? { 484 + items: valueSchemas, 485 + logicalOperator: 'or', 486 + } 487 + : valueSchemas[0]!, 488 + }, 489 + type: 'object', 490 + }; 504 491 505 - if (isRequired) { 506 - irDiscriminatorSchema.required = [discriminator.propertyName]; 507 - } 508 - schemaItems.push(irDiscriminatorSchema); 509 - addedDiscriminators.add(discriminator.propertyName); 492 + // Check if the discriminator property is required in any of the discriminator schemas 493 + // by looking at all discriminators with the same property name 494 + const isRequired = discriminators.some( 495 + (d) => 496 + d.discriminator.propertyName === discriminator.propertyName && 497 + // Check in the ref's required array or in the allOf components 498 + (ref.required?.includes(d.discriminator.propertyName) || 499 + (ref.allOf && 500 + ref.allOf.some((item) => { 501 + const resolvedItem = 502 + '$ref' in item 503 + ? context.resolveRef<SchemaObject>(item.$ref) 504 + : item; 505 + return resolvedItem.required?.includes( 506 + d.discriminator.propertyName, 507 + ); 508 + }))), 509 + ); 510 + 511 + if (isRequired) { 512 + irDiscriminatorSchema.required = [discriminator.propertyName]; 510 513 } 514 + schemaItems.push(irDiscriminatorSchema); 515 + addedDiscriminators.add(discriminator.propertyName); 511 516 } 512 517 } 513 518 }
+61 -56
packages/openapi-ts/src/openApi/3.1.x/parser/schema.ts
··· 84 84 }; 85 85 86 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. 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 89 */ 90 - const isSchemaExtendedInMapping = ({ 90 + const getAllDiscriminatorValues = ({ 91 91 context, 92 92 discriminator, 93 93 schemaRef, ··· 95 95 context: Context; 96 96 discriminator: NonNullable<SchemaObject['discriminator']>; 97 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 98 + }): string[] => { 99 + const values: string[] = []; 100 + 101 + // Check each entry in the discriminator mapping 102 + for (const [value, mappedSchemaRef] of Object.entries( 103 + discriminator.mapping || {}, 104 + )) { 102 105 if (mappedSchemaRef === schemaRef) { 106 + // This is the current schema's own value 107 + values.push(value); 103 108 continue; 104 109 } 105 110 106 - // Resolve the mapped schema 111 + // Check if the mapped schema extends the current schema 107 112 const mappedSchema = context.resolveRef<SchemaObject>(mappedSchemaRef); 108 - 109 - // Check if the mapped schema extends our schema via allOf 110 113 if (mappedSchema.allOf) { 111 114 for (const item of mappedSchema.allOf) { 112 115 if (item.$ref && item.$ref === schemaRef) { 113 - return true; 116 + // This schema extends the current schema, add its value 117 + values.push(value); 118 + break; 114 119 } 115 120 } 116 121 } 117 122 } 118 123 119 - return false; 124 + return values; 120 125 }; 121 126 122 127 const parseSchemaJsDoc = ({ ··· 537 542 ); 538 543 539 544 if (values.length > 0) { 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({ 545 + // For schemas that are extended by others, we need to include all child discriminator values 546 + // to create a union type (e.g., CarDto should have $type: 'Car' | 'Volvo') 547 + const allValues = getAllDiscriminatorValues({ 543 548 context, 544 549 discriminator, 545 550 schemaRef: state.$ref, 546 551 }); 547 552 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 - }; 553 + // Use allValues if we found children, otherwise use the original values 554 + const finalValues = allValues.length > 0 ? allValues : values; 567 555 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 - ); 556 + const valueSchemas: ReadonlyArray<IR.SchemaObject> = 557 + finalValues.map((value) => ({ 558 + const: value, 559 + type: 'string', 560 + })); 561 + const irDiscriminatorSchema: IR.SchemaObject = { 562 + properties: { 563 + [discriminator.propertyName]: 564 + valueSchemas.length > 1 565 + ? { 566 + items: valueSchemas, 567 + logicalOperator: 'or', 568 + } 569 + : valueSchemas[0]!, 570 + }, 571 + type: 'object', 572 + }; 585 573 586 - if (isRequired) { 587 - irDiscriminatorSchema.required = [discriminator.propertyName]; 588 - } 589 - schemaItems.push(irDiscriminatorSchema); 590 - addedDiscriminators.add(discriminator.propertyName); 574 + // Check if the discriminator property is required in any of the discriminator schemas 575 + // by looking at all discriminators with the same property name 576 + const isRequired = discriminators.some( 577 + (d) => 578 + d.discriminator.propertyName === discriminator.propertyName && 579 + // Check in the ref's required array or in the allOf components 580 + (ref.required?.includes(d.discriminator.propertyName) || 581 + (ref.allOf && 582 + ref.allOf.some((item) => { 583 + const resolvedItem = item.$ref 584 + ? context.resolveRef<SchemaObject>(item.$ref) 585 + : item; 586 + return resolvedItem.required?.includes( 587 + d.discriminator.propertyName, 588 + ); 589 + }))), 590 + ); 591 + 592 + if (isRequired) { 593 + irDiscriminatorSchema.required = [discriminator.propertyName]; 591 594 } 595 + schemaItems.push(irDiscriminatorSchema); 596 + addedDiscriminators.add(discriminator.propertyName); 592 597 } 593 598 } 594 599 }