Skip to content

Commit efb214e

Browse files
authored
fix(typescript-fetch): optional nullable fields return null instead of undefined (#24215)
* fix(typescript-fetch): optional nullable fields return null instead of undefined When an optional field is marked as nullable in the spec, FromJSONTyped now returns null (not undefined) when the API returns null. Non-nullable optional fields continue to return undefined as before. Fixes #5670 * fix(typescript-fetch): optional nullable fields return null instead of undefined Use strict equality (===) to distinguish between absent fields (undefined) and explicitly null fields. When an optional nullable field is absent from the JSON response, it returns undefined. When it is explicitly null, it returns null instead of undefined. Fixes #5670
1 parent 4bb04c3 commit efb214e

18 files changed

Lines changed: 106 additions & 57 deletions

File tree

modules/openapi-generator/src/main/resources/typescript-fetch/modelGeneric.mustache

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -87,45 +87,46 @@ export function {{classname}}FromJSONTyped(json: any, ignoreDiscriminator: boole
8787
{{#isPrimitiveType}}
8888
{{#isArray}}
8989
{{#uniqueItems}}
90-
'{{name}}': {{^required}}json['{{baseName}}'] == null ? undefined : {{/required}}{{#required}}{{#isNullable}}json['{{baseName}}'] == null ? null : {{/isNullable}}{{/required}}new Set(json['{{baseName}}']),
90+
{{! For optional nullable fields, return null (not undefined) when API returns null. See #5670 }}
91+
'{{name}}': {{^required}}{{#isNullable}}json['{{baseName}}'] === undefined ? undefined : json['{{baseName}}'] === null ? null : {{/isNullable}}{{^isNullable}}json['{{baseName}}'] == null ? undefined : {{/isNullable}}{{/required}}{{#required}}{{#isNullable}}json['{{baseName}}'] == null ? null : {{/isNullable}}{{/required}}new Set(json['{{baseName}}']),
9192
{{/uniqueItems}}
9293
{{^uniqueItems}}
93-
'{{name}}': {{^required}}json['{{baseName}}'] == null ? undefined : {{/required}}{{#required}}{{#isNullable}}json['{{baseName}}'] == null ? null : {{/isNullable}}{{/required}}json['{{baseName}}'],
94+
'{{name}}': {{^required}}{{#isNullable}}json['{{baseName}}'] === undefined ? undefined : json['{{baseName}}'] === null ? null : {{/isNullable}}{{^isNullable}}json['{{baseName}}'] == null ? undefined : {{/isNullable}}{{/required}}{{#required}}{{#isNullable}}json['{{baseName}}'] == null ? null : {{/isNullable}}{{/required}}json['{{baseName}}'],
9495
{{/uniqueItems}}
9596
{{/isArray}}
9697
{{^isArray}}
9798
{{#isDateType}}
98-
'{{name}}': {{^required}}json['{{baseName}}'] == null ? undefined : {{/required}}({{#required}}{{#isNullable}}json['{{baseName}}'] == null ? null : {{/isNullable}}{{/required}}new Date(json['{{baseName}}'])),
99+
'{{name}}': {{^required}}{{#isNullable}}json['{{baseName}}'] === undefined ? undefined : json['{{baseName}}'] === null ? null : {{/isNullable}}{{^isNullable}}json['{{baseName}}'] == null ? undefined : {{/isNullable}}{{/required}}({{#required}}{{#isNullable}}json['{{baseName}}'] == null ? null : {{/isNullable}}{{/required}}new Date(json['{{baseName}}'])),
99100
{{/isDateType}}
100101
{{#isDateTimeType}}
101-
'{{name}}': {{^required}}json['{{baseName}}'] == null ? undefined : {{/required}}({{#required}}{{#isNullable}}json['{{baseName}}'] == null ? null : {{/isNullable}}{{/required}}new Date(json['{{baseName}}'])),
102+
'{{name}}': {{^required}}{{#isNullable}}json['{{baseName}}'] === undefined ? undefined : json['{{baseName}}'] === null ? null : {{/isNullable}}{{^isNullable}}json['{{baseName}}'] == null ? undefined : {{/isNullable}}{{/required}}({{#required}}{{#isNullable}}json['{{baseName}}'] == null ? null : {{/isNullable}}{{/required}}new Date(json['{{baseName}}'])),
102103
{{/isDateTimeType}}
103104
{{^isDateType}}
104105
{{^isDateTimeType}}
105-
'{{name}}': {{^required}}json['{{baseName}}'] == null ? undefined : {{/required}}json['{{baseName}}'],
106+
'{{name}}': {{^required}}{{#isNullable}}json['{{baseName}}'] === undefined ? undefined : json['{{baseName}}'] === null ? null : {{/isNullable}}{{^isNullable}}json['{{baseName}}'] == null ? undefined : {{/isNullable}}{{/required}}json['{{baseName}}'],
106107
{{/isDateTimeType}}
107108
{{/isDateType}}
108109
{{/isArray}}
109110
{{/isPrimitiveType}}
110111
{{^isPrimitiveType}}
111112
{{#isArray}}
112113
{{#uniqueItems}}
113-
'{{name}}': {{^required}}json['{{baseName}}'] == null ? undefined : {{/required}}({{#required}}{{#isNullable}}json['{{baseName}}'] == null ? null : {{/isNullable}}{{/required}}new Set((json['{{baseName}}'] as Array<any>).map({{#items}}{{datatype}}{{/items}}FromJSON))),
114+
'{{name}}': {{^required}}{{#isNullable}}json['{{baseName}}'] === undefined ? undefined : json['{{baseName}}'] === null ? null : {{/isNullable}}{{^isNullable}}json['{{baseName}}'] == null ? undefined : {{/isNullable}}{{/required}}({{#required}}{{#isNullable}}json['{{baseName}}'] == null ? null : {{/isNullable}}{{/required}}new Set((json['{{baseName}}'] as Array<any>).map({{#items}}{{datatype}}{{/items}}FromJSON))),
114115
{{/uniqueItems}}
115116
{{^uniqueItems}}
116-
'{{name}}': {{^required}}json['{{baseName}}'] == null ? undefined : {{/required}}({{#required}}{{#isNullable}}json['{{baseName}}'] == null ? null : {{/isNullable}}{{/required}}(json['{{baseName}}'] as Array<any>).map({{#items}}{{datatype}}{{/items}}FromJSON)),
117+
'{{name}}': {{^required}}{{#isNullable}}json['{{baseName}}'] === undefined ? undefined : json['{{baseName}}'] === null ? null : {{/isNullable}}{{^isNullable}}json['{{baseName}}'] == null ? undefined : {{/isNullable}}{{/required}}({{#required}}{{#isNullable}}json['{{baseName}}'] == null ? null : {{/isNullable}}{{/required}}(json['{{baseName}}'] as Array<any>).map({{#items}}{{datatype}}{{/items}}FromJSON)),
117118
{{/uniqueItems}}
118119
{{/isArray}}
119120
{{#isMap}}
120-
'{{name}}': {{^required}}json['{{baseName}}'] == null ? undefined : {{/required}}({{#required}}{{#isNullable}}json['{{baseName}}'] == null ? null : {{/isNullable}}{{/required}}mapValues(json['{{baseName}}'], {{#items}}{{datatype}}{{/items}}FromJSON)),
121+
'{{name}}': {{^required}}{{#isNullable}}json['{{baseName}}'] === undefined ? undefined : json['{{baseName}}'] === null ? null : {{/isNullable}}{{^isNullable}}json['{{baseName}}'] == null ? undefined : {{/isNullable}}{{/required}}({{#required}}{{#isNullable}}json['{{baseName}}'] == null ? null : {{/isNullable}}{{/required}}mapValues(json['{{baseName}}'], {{#items}}{{datatype}}{{/items}}FromJSON)),
121122
{{/isMap}}
122123
{{^isArray}}
123124
{{^isMap}}
124125
{{^isFreeFormObject}}
125-
'{{name}}': {{^required}}json['{{baseName}}'] == null ? undefined : {{/required}}{{datatype}}FromJSON(json['{{baseName}}']),
126+
'{{name}}': {{^required}}{{#isNullable}}json['{{baseName}}'] === undefined ? undefined : json['{{baseName}}'] === null ? null : {{/isNullable}}{{^isNullable}}json['{{baseName}}'] == null ? undefined : {{/isNullable}}{{/required}}{{datatype}}FromJSON(json['{{baseName}}']),
126127
{{/isFreeFormObject}}
127128
{{#isFreeFormObject}}
128-
'{{name}}': {{^required}}json['{{baseName}}'] == null ? undefined : {{/required}}json['{{baseName}}'],
129+
'{{name}}': {{^required}}{{#isNullable}}json['{{baseName}}'] === undefined ? undefined : json['{{baseName}}'] === null ? null : {{/isNullable}}{{^isNullable}}json['{{baseName}}'] == null ? undefined : {{/isNullable}}{{/required}}json['{{baseName}}'],
129130
{{/isFreeFormObject}}
130131
{{/isMap}}
131132
{{/isArray}}

modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/fetch/TypeScriptFetchClientCodegenTest.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,20 @@ public void testNestedOneOfGeneratesTypeAliasForOneOfParent() throws IOException
653653
TestUtils.assertFileContains(outerPlain, "export interface OuterPlain {");
654654
}
655655

656+
@Test(description = "Optional nullable fields should deserialize to null, not undefined (fix #5670)")
657+
public void testOptionalNullableFieldDeserializesToNull() throws Exception {
658+
File output = generate(
659+
Collections.emptyMap(),
660+
"src/test/resources/3_0/typescript-fetch/nullable_property.json"
661+
);
662+
663+
Path modelPath = Paths.get(output + "/models/TestSchema.ts");
664+
// Optional nullable field: when API returns null, FromJSON should produce null not undefined
665+
TestUtils.assertFileContains(modelPath, "json['nullable_property'] === undefined ? undefined : json['nullable_property'] === null ? null :");
666+
// Required non-nullable field: still uses undefined path
667+
TestUtils.assertFileNotContains(modelPath, "json['required_property'] === undefined ? undefined : json['required_property'] === null ? null :");
668+
}
669+
656670
private static File generate(
657671
Map<String, Object> properties
658672
) throws IOException {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"openapi": "3.0.1",
3+
"info": {
4+
"title": "Optional Nullable Property Test",
5+
"version": "1.0.0"
6+
},
7+
"paths": {},
8+
"components": {
9+
"schemas": {
10+
"TestSchema": {
11+
"title": "TestSchema",
12+
"required": [
13+
"required_property"
14+
],
15+
"type": "object",
16+
"properties": {
17+
"required_property": {
18+
"title": "Required property",
19+
"type": "string"
20+
},
21+
"nullable_property": {
22+
"title": "Optional Nullable property",
23+
"type": "string",
24+
"nullable": true
25+
}
26+
}
27+
}
28+
}
29+
}
30+
}

samples/client/petstore/typescript-fetch/builds/allOf-nullable/models/Club.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export function ClubFromJSONTyped(json: any, ignoreDiscriminator: boolean): Club
5252
}
5353
return {
5454

55-
'owner': json['owner'] == null ? undefined : OwnerFromJSON(json['owner']),
55+
'owner': json['owner'] === undefined ? undefined : json['owner'] === null ? null : OwnerFromJSON(json['owner']),
5656
};
5757
}
5858

samples/client/petstore/typescript-fetch/builds/default-v3.0/models/EnumTest.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ export function EnumTestFromJSONTyped(json: any, ignoreDiscriminator: boolean):
160160
'enumStringRequired': json['enum_string_required'],
161161
'enumInteger': json['enum_integer'] == null ? undefined : json['enum_integer'],
162162
'enumNumber': json['enum_number'] == null ? undefined : json['enum_number'],
163-
'outerEnum': json['outerEnum'] == null ? undefined : OuterEnumFromJSON(json['outerEnum']),
163+
'outerEnum': json['outerEnum'] === undefined ? undefined : json['outerEnum'] === null ? null : OuterEnumFromJSON(json['outerEnum']),
164164
'outerEnumInteger': json['outerEnumInteger'] == null ? undefined : OuterEnumIntegerFromJSON(json['outerEnumInteger']),
165165
'outerEnumDefaultValue': json['outerEnumDefaultValue'] == null ? undefined : OuterEnumDefaultValueFromJSON(json['outerEnumDefaultValue']),
166166
'outerEnumIntegerDefaultValue': json['outerEnumIntegerDefaultValue'] == null ? undefined : OuterEnumIntegerDefaultValueFromJSON(json['outerEnumIntegerDefaultValue']),

samples/client/petstore/typescript-fetch/builds/default-v3.0/models/HealthCheckResult.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export function HealthCheckResultFromJSONTyped(json: any, ignoreDiscriminator: b
4444
}
4545
return {
4646

47-
'nullableMessage': json['NullableMessage'] == null ? undefined : json['NullableMessage'],
47+
'nullableMessage': json['NullableMessage'] === undefined ? undefined : json['NullableMessage'] === null ? null : json['NullableMessage'],
4848
};
4949
}
5050

samples/client/petstore/typescript-fetch/builds/default-v3.0/models/NullableClass.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -112,17 +112,17 @@ export function NullableClassFromJSONTyped(json: any, ignoreDiscriminator: boole
112112
return {
113113

114114
...json,
115-
'integerProp': json['integer_prop'] == null ? undefined : json['integer_prop'],
116-
'numberProp': json['number_prop'] == null ? undefined : json['number_prop'],
117-
'booleanProp': json['boolean_prop'] == null ? undefined : json['boolean_prop'],
118-
'stringProp': json['string_prop'] == null ? undefined : json['string_prop'],
119-
'dateProp': json['date_prop'] == null ? undefined : (new Date(json['date_prop'])),
120-
'datetimeProp': json['datetime_prop'] == null ? undefined : (new Date(json['datetime_prop'])),
121-
'arrayNullableProp': json['array_nullable_prop'] == null ? undefined : json['array_nullable_prop'],
122-
'arrayAndItemsNullableProp': json['array_and_items_nullable_prop'] == null ? undefined : json['array_and_items_nullable_prop'],
115+
'integerProp': json['integer_prop'] === undefined ? undefined : json['integer_prop'] === null ? null : json['integer_prop'],
116+
'numberProp': json['number_prop'] === undefined ? undefined : json['number_prop'] === null ? null : json['number_prop'],
117+
'booleanProp': json['boolean_prop'] === undefined ? undefined : json['boolean_prop'] === null ? null : json['boolean_prop'],
118+
'stringProp': json['string_prop'] === undefined ? undefined : json['string_prop'] === null ? null : json['string_prop'],
119+
'dateProp': json['date_prop'] === undefined ? undefined : json['date_prop'] === null ? null : (new Date(json['date_prop'])),
120+
'datetimeProp': json['datetime_prop'] === undefined ? undefined : json['datetime_prop'] === null ? null : (new Date(json['datetime_prop'])),
121+
'arrayNullableProp': json['array_nullable_prop'] === undefined ? undefined : json['array_nullable_prop'] === null ? null : json['array_nullable_prop'],
122+
'arrayAndItemsNullableProp': json['array_and_items_nullable_prop'] === undefined ? undefined : json['array_and_items_nullable_prop'] === null ? null : json['array_and_items_nullable_prop'],
123123
'arrayItemsNullable': json['array_items_nullable'] == null ? undefined : json['array_items_nullable'],
124-
'objectNullableProp': json['object_nullable_prop'] == null ? undefined : json['object_nullable_prop'],
125-
'objectAndItemsNullableProp': json['object_and_items_nullable_prop'] == null ? undefined : json['object_and_items_nullable_prop'],
124+
'objectNullableProp': json['object_nullable_prop'] === undefined ? undefined : json['object_nullable_prop'] === null ? null : json['object_nullable_prop'],
125+
'objectAndItemsNullableProp': json['object_and_items_nullable_prop'] === undefined ? undefined : json['object_and_items_nullable_prop'] === null ? null : json['object_and_items_nullable_prop'],
126126
'objectItemsNullable': json['object_items_nullable'] == null ? undefined : json['object_items_nullable'],
127127
};
128128
}

samples/client/petstore/typescript-fetch/builds/default-v3.0/models/ParentWithNullable.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export function ParentWithNullableFromJSONTyped(json: any, ignoreDiscriminator:
6868
return {
6969

7070
'type': json['type'] == null ? undefined : json['type'],
71-
'nullableProperty': json['nullableProperty'] == null ? undefined : json['nullableProperty'],
71+
'nullableProperty': json['nullableProperty'] === undefined ? undefined : json['nullableProperty'] === null ? null : json['nullableProperty'],
7272
};
7373
}
7474

samples/client/petstore/typescript-fetch/builds/enum/models/EnumPatternObject.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,9 @@ export function EnumPatternObjectFromJSONTyped(json: any, ignoreDiscriminator: b
8080
return {
8181

8282
'stringEnum': json['string-enum'] == null ? undefined : StringEnumFromJSON(json['string-enum']),
83-
'nullableStringEnum': json['nullable-string-enum'] == null ? undefined : StringEnumFromJSON(json['nullable-string-enum']),
83+
'nullableStringEnum': json['nullable-string-enum'] === undefined ? undefined : json['nullable-string-enum'] === null ? null : StringEnumFromJSON(json['nullable-string-enum']),
8484
'numberEnum': json['number-enum'] == null ? undefined : NumberEnumFromJSON(json['number-enum']),
85-
'nullableNumberEnum': json['nullable-number-enum'] == null ? undefined : NumberEnumFromJSON(json['nullable-number-enum']),
85+
'nullableNumberEnum': json['nullable-number-enum'] === undefined ? undefined : json['nullable-number-enum'] === null ? null : NumberEnumFromJSON(json['nullable-number-enum']),
8686
};
8787
}
8888

samples/client/petstore/typescript-fetch/builds/kebab-case/models/enum-test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ export function EnumTestFromJSONTyped(json: any, ignoreDiscriminator: boolean):
160160
'enumStringRequired': json['enum_string_required'],
161161
'enumInteger': json['enum_integer'] == null ? undefined : json['enum_integer'],
162162
'enumNumber': json['enum_number'] == null ? undefined : json['enum_number'],
163-
'outerEnum': json['outerEnum'] == null ? undefined : OuterEnumFromJSON(json['outerEnum']),
163+
'outerEnum': json['outerEnum'] === undefined ? undefined : json['outerEnum'] === null ? null : OuterEnumFromJSON(json['outerEnum']),
164164
'outerEnumInteger': json['outerEnumInteger'] == null ? undefined : OuterEnumIntegerFromJSON(json['outerEnumInteger']),
165165
'outerEnumDefaultValue': json['outerEnumDefaultValue'] == null ? undefined : OuterEnumDefaultValueFromJSON(json['outerEnumDefaultValue']),
166166
'outerEnumIntegerDefaultValue': json['outerEnumIntegerDefaultValue'] == null ? undefined : OuterEnumIntegerDefaultValueFromJSON(json['outerEnumIntegerDefaultValue']),

0 commit comments

Comments
 (0)