Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,41 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
}
}
}

// Detect if discriminator variants already declare the discriminator property.
// When they do, the union type should use simple unions (e.g., ApiKey | Basic)
// instead of intersection wrappers (e.g., { type: 'APIKEY' } & ApiKey), because
// the intersection of a string literal with a string enum evaluates to `never`
// in TypeScript when stringEnums is enabled.
for (ExtendedCodegenModel rootModel : allModels) {
CodegenDiscriminator discriminator = rootModel.discriminator;
boolean hasDiscriminator = discriminator != null;
boolean hasMappedModels = hasDiscriminator
&& discriminator.getMappedModels() != null
&& !discriminator.getMappedModels().isEmpty();
if (!hasMappedModels) {
continue;
}
String discPropBaseName = discriminator.getPropertyBaseName();
boolean allVariantsHaveDiscriminator = true;
for (CodegenDiscriminator.MappedModel mm : discriminator.getMappedModels()) {
boolean variantDeclaresDiscriminatorProperty = allModels.stream()
.filter(model -> model.classname.equals(mm.getModelName()))
.findFirst()
.map(model -> model.vars.stream()
.anyMatch(v -> v.baseName.equals(discPropBaseName)))
.orElse(false);
if (!variantDeclaresDiscriminatorProperty) {
allVariantsHaveDiscriminator = false;
break;
}
}
if (allVariantsHaveDiscriminator) {
discriminator.getVendorExtensions()
.put("x-variants-have-discriminator", true);
}
}

return result;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@ export function {{classname}}FromJSONTyped(json: any, ignoreDiscriminator: boole
switch (json['{{discriminator.propertyBaseName}}']) {
{{#discriminator.mappedModels}}
case '{{mappingName}}':
{{#discriminator.vendorExtensions.x-variants-have-discriminator}}
return {{modelName}}FromJSONTyped(json, true);
{{/discriminator.vendorExtensions.x-variants-have-discriminator}}
{{^discriminator.vendorExtensions.x-variants-have-discriminator}}
return Object.assign({}, {{modelName}}FromJSONTyped(json, true), { {{discriminator.propertyName}}: '{{mappingName}}' } as const);
{{/discriminator.vendorExtensions.x-variants-have-discriminator}}
{{/discriminator.mappedModels}}
default:
return json;
Expand Down Expand Up @@ -151,7 +156,12 @@ export function {{classname}}ToJSONTyped(value?: {{classname}} | null, ignoreDis
switch (value['{{discriminator.propertyName}}']) {
{{#discriminator.mappedModels}}
case '{{mappingName}}':
{{#discriminator.vendorExtensions.x-variants-have-discriminator}}
return {{modelName}}ToJSON(value);
{{/discriminator.vendorExtensions.x-variants-have-discriminator}}
{{^discriminator.vendorExtensions.x-variants-have-discriminator}}
return Object.assign({}, {{modelName}}ToJSON(value), { {{discriminator.propertyName}}: '{{mappingName}}' } as const);
{{/discriminator.vendorExtensions.x-variants-have-discriminator}}
{{/discriminator.mappedModels}}
default:
return value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
* {{#lambda.indented_star_1}}{{{unescapedDescription}}}{{/lambda.indented_star_1}}
* @export
*/
export type {{classname}} = {{#discriminator}}{{#mappedModels}}{ {{discriminator.propertyName}}: '{{mappingName}}' } & {{modelName}}{{^-last}} | {{/-last}}{{/mappedModels}}{{/discriminator}}{{^discriminator}}{{#oneOf}}{{{.}}}{{^-last}} | {{/-last}}{{/oneOf}}{{/discriminator}};
export type {{classname}} = {{#discriminator}}{{#vendorExtensions.x-variants-have-discriminator}}{{#mappedModels}}{{modelName}}{{^-last}} | {{/-last}}{{/mappedModels}}{{/vendorExtensions.x-variants-have-discriminator}}{{^vendorExtensions.x-variants-have-discriminator}}{{#mappedModels}}{ {{discriminator.propertyName}}: '{{mappingName}}' } & {{modelName}}{{^-last}} | {{/-last}}{{/mappedModels}}{{/vendorExtensions.x-variants-have-discriminator}}{{/discriminator}}{{^discriminator}}{{#oneOf}}{{{.}}}{{^-last}} | {{/-last}}{{/oneOf}}{{/discriminator}};
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,14 @@ public void testOneOfModelsDoNotImportPrimitiveTypes() throws IOException {
TestUtils.assertFileExists(testDiscriminatorResponse);
TestUtils.assertFileContains(testDiscriminatorResponse, "import type { OptionOne } from './OptionOne'");
TestUtils.assertFileContains(testDiscriminatorResponse, "import type { OptionTwo } from './OptionTwo'");
TestUtils.assertFileContains(testDiscriminatorResponse, "export type TestDiscriminatorResponse = { discriminatorField: 'optionOne' } & OptionOne | { discriminatorField: 'optionTwo' } & OptionTwo");
// When variants already declare the discriminator property (OptionOne/OptionTwo have
// discriminatorField as a required single-value enum), the union should use simple types
// instead of intersection wrappers to avoid TypeScript `never` type issues with string enums
TestUtils.assertFileContains(testDiscriminatorResponse, "export type TestDiscriminatorResponse = OptionOne | OptionTwo");
TestUtils.assertFileNotContains(testDiscriminatorResponse, "{ discriminatorField: 'optionOne' } & OptionOne");
// FromJSON should delegate directly without Object.assign wrapper
TestUtils.assertFileContains(testDiscriminatorResponse, "return OptionOneFromJSONTyped(json, true)");
TestUtils.assertFileNotContains(testDiscriminatorResponse, "Object.assign");
}

/**
Expand Down Expand Up @@ -444,6 +451,21 @@ public void testOneOfModelsImportNonPrimitiveTypes() throws IOException {
TestUtils.assertFileContains(testResponse, "import type { OptionThree } from './OptionThree'");
}

@Test(description = "Verify discriminator uses intersection wrappers when variants do NOT have discriminator property")
public void testDiscriminatorWithoutPropertyOnVariantsUsesIntersectionWrapper() throws IOException {
File output = generate(
Collections.emptyMap(),
"src/test/resources/3_0/typescript-fetch/discriminator-without-property.yaml"
);

Path shapePath = Paths.get(output + "/models/Shape.ts");
TestUtils.assertFileExists(shapePath);
// When variants don't have the discriminator property, intersection wrappers should be used
TestUtils.assertFileContains(shapePath, "{ shapeType: 'circle' } & Circle");
TestUtils.assertFileContains(shapePath, "{ shapeType: 'square' } & Square");
TestUtils.assertFileContains(shapePath, "Object.assign");
}

@Test(description = "Verify validationAttributes works with withoutRuntimeChecks=true")
public void testValidationAttributesWithWithoutRuntimeChecks() throws IOException {
Map<String, Object> properties = new HashMap<>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
openapi: 3.0.0
info:
version: 1.0.0
title: testing discriminator when variants do NOT have discriminator property
paths:
/test:
get:
operationId: test
responses:
200:
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Shape'
components:
schemas:
Shape:
discriminator:
propertyName: shapeType
mapping:
circle: "#/components/schemas/Circle"
square: "#/components/schemas/Square"
oneOf:
- $ref: "#/components/schemas/Circle"
- $ref: "#/components/schemas/Square"
Circle:
type: object
properties:
radius:
type: number
required:
- radius
Square:
type: object
properties:
side:
type: number
required:
- side
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
*
* @export
*/
export type TestDiscriminatorResponse = { discriminatorField: 'optionOne' } & OptionOne | { discriminatorField: 'optionTwo' } & OptionTwo;
export type TestDiscriminatorResponse = OptionOne | OptionTwo;

export function TestDiscriminatorResponseFromJSON(json: any): TestDiscriminatorResponse {
return TestDiscriminatorResponseFromJSONTyped(json, false);
Expand All @@ -44,9 +44,9 @@ export function TestDiscriminatorResponseFromJSONTyped(json: any, ignoreDiscrimi
}
switch (json['discriminatorField']) {
case 'optionOne':
return Object.assign({}, OptionOneFromJSONTyped(json, true), { discriminatorField: 'optionOne' } as const);
return OptionOneFromJSONTyped(json, true);
case 'optionTwo':
return Object.assign({}, OptionTwoFromJSONTyped(json, true), { discriminatorField: 'optionTwo' } as const);
return OptionTwoFromJSONTyped(json, true);
default:
return json;
}
Expand All @@ -62,9 +62,9 @@ export function TestDiscriminatorResponseToJSONTyped(value?: TestDiscriminatorRe
}
switch (value['discriminatorField']) {
case 'optionOne':
return Object.assign({}, OptionOneToJSON(value), { discriminatorField: 'optionOne' } as const);
return OptionOneToJSON(value);
case 'optionTwo':
return Object.assign({}, OptionTwoToJSON(value), { discriminatorField: 'optionTwo' } as const);
return OptionTwoToJSON(value);
default:
return value;
}
Expand Down
Loading