[typescript-fetch] oneOf models now consider primitives when converting. Issue #21259 (#21464)

* [typescript-fetch] number, string, and Date now considered in oneOf models. Issue #21259

* Generated samples
This commit is contained in:
DavidGrath 2025-07-24 07:05:06 +01:00 committed by GitHub
parent 777b7eeea0
commit f1a093537d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 232 additions and 6 deletions

View File

@ -799,6 +799,11 @@ public class TypeScriptFetchClientCodegen extends AbstractTypeScriptClientCodege
.filter(Objects::nonNull)
.collect(Collectors.toCollection(TreeSet::new));
cm.oneOfPrimitives = oneOfsList.stream()
.filter(CodegenProperty::getIsPrimitiveType)
.filter(Objects::nonNull)
.collect(Collectors.toCollection(HashSet::new));
if (!cm.oneOf.isEmpty()) {
// For oneOfs only import $refs within the oneOf
cm.imports = cm.imports.stream()
@ -1484,6 +1489,8 @@ public class TypeScriptFetchClientCodegen extends AbstractTypeScriptClientCodege
public Set<String> oneOfModels = new TreeSet<>();
@Getter @Setter
public Set<String> oneOfArrays = new TreeSet<>();
@Getter @Setter
public Set<CodegenProperty> oneOfPrimitives = new HashSet<>();
public boolean isEntity; // Is a model containing an "id" property marked as isUniqueId
public String returnPassthrough;

View File

@ -65,7 +65,64 @@ export function {{classname}}FromJSONTyped(json: any, ignoreDiscriminator: boole
}
{{/-last}}
{{/oneOfArrays}}
{{#oneOfPrimitives}}
{{#isArray}}
{{#items}}
{{#isDateType}}
if (Array.isArray(json)) {
if (json.every(item => !(isNaN(new Date(json).getTime()))) {
return json.map(value => new Date(json);
}
}
{{/isDateType}}
{{#isDateTimeType}}
if (Array.isArray(json)) {
if (json.every(item => !(isNaN(new Date(json).getTime()))) {
return json.map(value => new Date(json);
}
}
{{/isDateTimeType}}
{{#isNumeric}}
if (Array.isArray(json)) {
if (json.every(item => typeof item === 'number'{{#isEnum}} && ({{#allowableValues}}{{#values}}item === {{.}}{{^-last}} || {{/-last}}{{/values}}{{/allowableValues}}){{/isEnum}})) {
return json;
}
}
{{/isNumeric}}
{{#isString}}
if (Array.isArray(json)) {
if (json.every(item => typeof item === 'string'{{#isEnum}} && ({{#allowableValues}}{{#values}}item === '{{.}}'{{^-last}} || {{/-last}}{{/values}}{{/allowableValues}}){{/isEnum}})) {
return json;
}
}
{{/isString}}
{{/items}}
{{/isArray}}
{{^isArray}}
{{#isDateType}}
if(!(isNaN(new Date(json).getTime()))) {
return {{^required}}json == null ? undefined : {{/required}}({{#required}}{{#isNullable}}json == null ? null : {{/isNullable}}{{/required}}new Date(json));
}
{{/isDateType}}
{{^isDateType}}
{{#isDateTimeType}}
if(!(isNaN(new Date(json).getTime()))) {
return {{^required}}json == null ? undefined : {{/required}}({{#required}}{{#isNullable}}json == null ? null : {{/isNullable}}{{/required}}new Date(json));
}
{{/isDateTimeType}}
{{/isDateType}}
{{#isNumeric}}
if(typeof json === 'number'{{#isEnum}} && ({{#allowableValues}}{{#values}}json === {{.}}{{^-last}} || {{/-last}}{{/values}}{{/allowableValues}}){{/isEnum}}) {
return json;
}
{{/isNumeric}}
{{#isString}}
if(typeof json === 'string'{{#isEnum}} && ({{#allowableValues}}{{#values}}json === '{{.}}'{{^-last}} || {{/-last}}{{/values}}{{/allowableValues}}){{/isEnum}}) {
return json;
}
{{/isString}}
{{/isArray}}
{{/oneOfPrimitives}}
return {} as any;
{{/discriminator}}
}
@ -113,7 +170,62 @@ export function {{classname}}ToJSONTyped(value?: {{classname}} | null, ignoreDis
}
{{/-last}}
{{/oneOfArrays}}
{{#oneOfPrimitives}}
{{#isArray}}
{{#items}}
{{#isDateType}}
if (Array.isArray(value)) {
if (value.every(item => item instanceof Date) {
return value.map(value => value.toISOString().substring(0,10)));
}
}
{{/isDateType}}
{{#isDateTimeType}}
if (Array.isArray(value)) {
if (value.every(item => item instanceof Date) {
return value.map(value => value.toISOString();
}
}
{{/isDateTimeType}}
{{#isNumeric}}
if (Array.isArray(value)) {
if (value.every(item => typeof item === 'number'{{#isEnum}} && ({{#allowableValues}}{{#values}}item === {{.}}{{^-last}} || {{/-last}}{{/values}}{{/allowableValues}}){{/isEnum}}) {
return value;
}
}
{{/isNumeric}}
{{#isString}}
if (Array.isArray(value)) {
if (value.every(item => typeof item === 'string'{{#isEnum}} && ({{#allowableValues}}{{#values}}item === '{{.}}'{{^-last}} || {{/-last}}{{/values}}{{/allowableValues}}){{/isEnum}}) {
return value;
}
}
{{/isString}}
{{/items}}
{{/isArray}}
{{^isArray}}
{{#isDateType}}
if(value instanceof Date) {
return ((value{{#isNullable}} as any{{/isNullable}}){{^required}}{{#isNullable}}?{{/isNullable}}{{/required}}.toISOString().substring(0,10));
}
{{/isDateType}}
{{#isDateTimeType}}
if(value instanceof Date) {
return {{^required}}{{#isNullable}}value === null ? null : {{/isNullable}}{{^isNullable}}value == null ? undefined : {{/isNullable}}{{/required}}((value{{#isNullable}} as any{{/isNullable}}){{^required}}{{#isNullable}}?{{/isNullable}}{{/required}}.toISOString());
}
{{/isDateTimeType}}
{{#isNumeric}}
if(typeof value === 'number'{{#isEnum}} && ({{#allowableValues}}{{#values}}value === {{.}}{{^-last}} || {{/-last}}{{/values}}{{/allowableValues}}){{/isEnum}}) {
return value;
}
{{/isNumeric}}
{{#isString}}
if(typeof value === 'string'{{#isEnum}} && ({{#allowableValues}}{{#values}}value === '{{.}}'{{^-last}} || {{/-last}}{{/values}}{{/allowableValues}}){{/isEnum}}) {
return value;
}
{{/isString}}
{{/isArray}}
{{/oneOfPrimitives}}
return {};
{{/discriminator}}
}

View File

@ -347,6 +347,39 @@ public class TypeScriptFetchClientCodegenTest {
TestUtils.assertFileExists(Paths.get(output + "/apis/petControllerApi.ts"));
}
@Test(description = "Issue #21295")
public void givenSchemaIsOneOfAndComposedSchemasArePrimitiveThenReturnStatementsAreCorrect() throws Exception {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
output.deleteOnExit();
String outputPath = output.getAbsolutePath();
TypeScriptFetchClientCodegen clientCodegen = new TypeScriptFetchClientCodegen();
clientCodegen.setOutputDir(outputPath);
DefaultGenerator defaultGenerator = new DefaultGenerator();
defaultGenerator.opts(
new ClientOptInput().openAPI(TestUtils.parseSpec("src/test/resources/bugs/issue_21259.yaml"))
.config(clientCodegen)
).generate();
Path exampleModelPath = Paths.get(outputPath + "/models/MyCustomSpeed.ts");
//FromJSON
TestUtils.assertFileContains(exampleModelPath, "typeof json === 'number'");
TestUtils.assertFileContains(exampleModelPath, "typeof json === 'string'");
TestUtils.assertFileContains(exampleModelPath, "json === 'fixed-value-a' || json === 'fixed-value-b' || json === 'fixed-value-c'");
TestUtils.assertFileContains(exampleModelPath, "isNaN(new Date(json).getTime())");
TestUtils.assertFileContains(exampleModelPath, "json.every(item => typeof item === 'number'");
// TestUtils.assertFileContains(exampleModelPath, "json.every(item => typeof item === 'string' && (item === 'oneof-array-enum-a' || item oneof-array-enum-b || item === oneof-array-enum-c)");
//ToJSON
TestUtils.assertFileContains(exampleModelPath, "typeof value === 'number'");
TestUtils.assertFileContains(exampleModelPath, "typeof value === 'string'");
TestUtils.assertFileContains(exampleModelPath, "value === 'fixed-value-a' || value === 'fixed-value-b' || value === 'fixed-value-c'");
TestUtils.assertFileContains(exampleModelPath, "value instanceof Date");
TestUtils.assertFileContains(exampleModelPath, "value.every(item => typeof item === 'number'");
// TestUtils.assertFileContains(exampleModelPath, "value.every(item => typeof item === 'string' && (item === 'oneof-array-enum-a' || item oneof-array-enum-b || item === oneof-array-enum-c)");
}
private static File generate(Map<String, Object> properties) throws IOException {
File output = Files.createTempDirectory("test").toFile();
output.deleteOnExit();

View File

@ -0,0 +1,62 @@
#Modified from the original
openapi: 3.0.1
info:
title: Minimal API for Bug Report
version: v1
paths:
/test:
post:
summary: Test endpoint with MyCustomSpeed
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TestPayload'
responses:
'200':
description: OK
components:
schemas:
MyNumericValue:
type: object
properties:
lmnop:
type: number
description: A numeric value (e.g., 0 to 1).
MyCustomSpeed:
oneOf:
- $ref: '#/components/schemas/MyNumericValue'
- type: string
enum:
- "fixed-value-a"
- "fixed-value-b"
- "fixed-value-c"
- type: string
format: date
- type: string
format: date-time
- type: integer
format: int64
enum: [10, 20, 30]
- type: array
items:
type: number
- type: array
items:
- type: object
- type: array
items:
- type: string
enum:
# It seems enums within arrays don't work. Leaving this here, though
- "oneof-array-enum-a"
- "oneof-array-enum-b"
- "oneof-array-enum-c"
- type:
description: A value that can be a number or a specific string.
TestPayload:
type: object
properties:
speed_setting:
$ref: '#/components/schemas/MyCustomSpeed'

View File

@ -53,7 +53,11 @@ export function TestArrayResponseFromJSONTyped(json: any, ignoreDiscriminator: b
}
return json;
}
if (Array.isArray(json)) {
if (json.every(item => typeof item === 'string')) {
return json;
}
}
return {} as any;
}
@ -76,7 +80,11 @@ export function TestArrayResponseToJSONTyped(value?: TestArrayResponse | null, i
}
return value;
}
if (Array.isArray(value)) {
if (value.every(item => typeof item === 'string') {
return value;
}
}
return {};
}

View File

@ -51,7 +51,9 @@ export function TestResponseFromJSONTyped(json: any, ignoreDiscriminator: boolea
if (instanceOfTestB(json)) {
return TestBFromJSONTyped(json, true);
}
if(typeof json === 'string') {
return json;
}
return {} as any;
}
@ -72,7 +74,9 @@ export function TestResponseToJSONTyped(value?: TestResponse | null, ignoreDiscr
if (instanceOfTestB(value)) {
return TestBToJSON(value as TestB);
}
if(typeof value === 'string') {
return value;
}
return {};
}