Skip to content

Commit 4469208

Browse files
committed
fix(dart): preserve nullable nested array item types and decoding
1 parent 8d32203 commit 4469208

15 files changed

Lines changed: 147 additions & 32 deletions

File tree

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractDartCodegen.java

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -554,13 +554,17 @@ public String toDefaultValue(Schema schema) {
554554

555555
@Override
556556
public String getTypeDeclaration(Schema p) {
557+
return getTypeDeclaration(p, false);
558+
}
559+
560+
private String getTypeDeclaration(Schema p, boolean includeNullableSuffix) {
557561
Schema<?> schema = unaliasSchema(p);
558562
Schema<?> target = ModelUtils.isGenerateAliasAsModel() ? p : schema;
563+
String typeDeclaration;
559564
if (ModelUtils.isArraySchema(target)) {
560565
Schema<?> items = ModelUtils.getSchemaItems(schema);
561-
return getSchemaType(target) + "<" + getTypeDeclaration(items) + ">";
562-
}
563-
if (ModelUtils.isMapSchema(target)) {
566+
typeDeclaration = getSchemaType(target) + "<" + getTypeDeclaration(items, true) + ">";
567+
} else if (ModelUtils.isMapSchema(target)) {
564568
// Note: ModelUtils.isMapSchema(p) returns true when p is a composed schema that also defines
565569
// additionalproperties: true
566570
Schema<?> inner = ModelUtils.getAdditionalProperties(target);
@@ -569,9 +573,16 @@ public String getTypeDeclaration(Schema p) {
569573
inner = new StringSchema().description("TODO default missing map inner type to string");
570574
p.setAdditionalProperties(inner);
571575
}
572-
return getSchemaType(target) + "<String, " + getTypeDeclaration(inner) + ">";
576+
typeDeclaration = getSchemaType(target) + "<String, " + getTypeDeclaration(inner, true) + ">";
577+
} else {
578+
typeDeclaration = super.getTypeDeclaration(p);
573579
}
574-
return super.getTypeDeclaration(p);
580+
581+
if (includeNullableSuffix && ModelUtils.isNullable(schema)) {
582+
return typeDeclaration + "?";
583+
}
584+
585+
return typeDeclaration;
575586
}
576587

577588
@Override

modules/openapi-generator/src/main/resources/dart2/serialization/native/native_class.mustache

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -198,10 +198,10 @@ class {{{classname}}} {
198198
{{{name}}}: json[r'{{{baseName}}}'] is List
199199
? (json[r'{{{baseName}}}'] as List).map((e) =>
200200
{{#items.complexType}}
201-
{{items.complexType}}.listFromJson(json[r'{{{baseName}}}']){{#uniqueItems}}.toSet(){{/uniqueItems}}
201+
e == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.complexType}}>[]{{/items.isNullable}} : {{items.complexType}}.listFromJson(e){{#uniqueItems}}.toSet(){{/uniqueItems}}
202202
{{/items.complexType}}
203203
{{^items.complexType}}
204-
e == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.items.dataType}}>[]{{/items.isNullable}} : (e as List).cast<{{items.items.dataType}}>()
204+
e == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.items.dataType}}{{#items.items.isNullable}}?{{/items.items.isNullable}}>[]{{/items.isNullable}} : (e as List).map((value) => value as {{items.items.dataType}}{{#items.items.isNullable}}?{{/items.items.isNullable}}).toList(growable: false)
205205
{{/items.complexType}}
206206
).toList()
207207
: {{#isNullable}}null{{/isNullable}}{{^isNullable}}const []{{/isNullable}},
@@ -219,7 +219,7 @@ class {{{classname}}} {
219219
: {{items.complexType}}.mapListFromJson(json[r'{{{baseName}}}']),
220220
{{/items.complexType}}
221221
{{^items.complexType}}
222-
: (json[r'{{{baseName}}}'] as Map<String, dynamic>).map((k, v) => MapEntry(k, v == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.items.dataType}}>[]{{/items.isNullable}} : (v as List).cast<{{items.items.dataType}}>())),
222+
: (json[r'{{{baseName}}}'] as Map<String, dynamic>).map((k, v) => MapEntry(k, v == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.items.dataType}}{{#items.items.isNullable}}?{{/items.items.isNullable}}>[]{{/items.isNullable}} : (v as List).map((value) => value as {{items.items.dataType}}{{#items.items.isNullable}}?{{/items.items.isNullable}}).toList(growable: false))),
223223
{{/items.complexType}}
224224
{{/items.isArray}}
225225
{{^items.isArray}}

modules/openapi-generator/src/test/java/org/openapitools/codegen/dart/DartClientCodegenTest.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,4 +169,18 @@ public void testRequiredNullableFieldsDoNotAssertNonNull() throws Exception {
169169
TestUtils.assertFileNotContains(modelFile.toPath(),
170170
"json[r'nickname'] != null");
171171
}
172+
173+
@Test(description = "Nullable nested arrays of complex types should preserve null entries")
174+
public void testNullableNestedComplexArraysPreserveNullEntries() throws Exception {
175+
List<File> files = generateDartNativeFromSpec(
176+
"src/test/resources/3_0/dart/dart-native-deserialization-bugs.yaml");
177+
178+
File modelFile = files.stream()
179+
.filter(f -> f.getName().equals("complex_nested_array_nullable_model.dart"))
180+
.findFirst()
181+
.orElseThrow(() -> new AssertionError("complex_nested_array_nullable_model.dart not found in generated files"));
182+
183+
TestUtils.assertFileContains(modelFile.toPath(),
184+
"e == null ? null : NullableRequiredModel.listFromJson(e)");
185+
}
172186
}

modules/openapi-generator/src/test/java/org/openapitools/codegen/dart/DartModelTest.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,4 +716,28 @@ public void testParameterUnwrappingBehavior() {
716716
Assert.assertFalse(filterParam.dataType.startsWith("Optional<"),
717717
"Query parameter should not be wrapped with Optional");
718718
}
719+
720+
@Test(description = "array items can be nullable")
721+
public void arrayItemsCanBeNullable() {
722+
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/array-nullable-items.yaml");
723+
final DefaultCodegen codegen = new DartClientCodegen();
724+
codegen.setOpenAPI(openAPI);
725+
final ArraySchema schema = (ArraySchema) openAPI.getComponents().getSchemas().get("ArrayWithNullableItemsModel")
726+
.getProperties()
727+
.get("foo");
728+
729+
Assert.assertEquals(codegen.getTypeDeclaration(schema), "List<String?>");
730+
}
731+
732+
@Test(description = "nested array items can be nullable")
733+
public void nestedArrayItemsCanBeNullable() {
734+
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/nested-array-nullable-items.yaml");
735+
final DefaultCodegen codegen = new DartClientCodegen();
736+
codegen.setOpenAPI(openAPI);
737+
final ArraySchema schema = (ArraySchema) openAPI.getComponents().getSchemas().get("NestedArrayWithNullableItemsModel")
738+
.getProperties()
739+
.get("foo");
740+
741+
Assert.assertEquals(codegen.getTypeDeclaration(schema), "List<List<String?>>");
742+
}
719743
}

modules/openapi-generator/src/test/java/org/openapitools/codegen/dart/dio/DartDioModelTest.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,4 +492,32 @@ public void dateDefaultValues() {
492492
Assert.assertEquals(dateTimeDefault.name, "dateTime");
493493
Assert.assertNull(dateTimeDefault.defaultValue);
494494
}
495+
496+
@Test(description = "array items can be nullable")
497+
public void arrayItemsCanBeNullable() {
498+
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/array-nullable-items.yaml");
499+
final DartDioClientCodegen codegen = new DartDioClientCodegen();
500+
codegen.additionalProperties().put(CodegenConstants.SERIALIZATION_LIBRARY, DartDioClientCodegen.SERIALIZATION_LIBRARY_BUILT_VALUE);
501+
codegen.processOpts();
502+
codegen.setOpenAPI(openAPI);
503+
final ArraySchema schema = (ArraySchema) openAPI.getComponents().getSchemas().get("ArrayWithNullableItemsModel")
504+
.getProperties()
505+
.get("foo");
506+
507+
Assert.assertEquals(codegen.getTypeDeclaration(schema), "BuiltList<String?>");
508+
}
509+
510+
@Test(description = "nested array items can be nullable")
511+
public void nestedArrayItemsCanBeNullable() {
512+
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/nested-array-nullable-items.yaml");
513+
final DartDioClientCodegen codegen = new DartDioClientCodegen();
514+
codegen.additionalProperties().put(CodegenConstants.SERIALIZATION_LIBRARY, DartDioClientCodegen.SERIALIZATION_LIBRARY_BUILT_VALUE);
515+
codegen.processOpts();
516+
codegen.setOpenAPI(openAPI);
517+
final ArraySchema schema = (ArraySchema) openAPI.getComponents().getSchemas().get("NestedArrayWithNullableItemsModel")
518+
.getProperties()
519+
.get("foo");
520+
521+
Assert.assertEquals(codegen.getTypeDeclaration(schema), "BuiltList<BuiltList<String?>>");
522+
}
495523
}

modules/openapi-generator/src/test/resources/3_0/dart/dart-native-deserialization-bugs.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,13 @@ components:
3232
nickname:
3333
type: string
3434
nullable: true
35+
ComplexNestedArrayNullableModel:
36+
type: object
37+
properties:
38+
matrix:
39+
type: array
40+
items:
41+
type: array
42+
nullable: true
43+
items:
44+
$ref: '#/components/schemas/NullableRequiredModel'
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
openapi: 3.0.0
2+
info:
3+
title: Nested array nullable items
4+
version: latest
5+
paths:
6+
'/':
7+
get:
8+
operationId: operation
9+
responses:
10+
'200':
11+
description: Success
12+
content:
13+
application/json:
14+
schema:
15+
$ref: '#/components/schemas/NestedArrayWithNullableItemsModel'
16+
components:
17+
schemas:
18+
NestedArrayWithNullableItemsModel:
19+
required:
20+
- foo
21+
properties:
22+
foo:
23+
type: array
24+
items:
25+
type: array
26+
items:
27+
type: string
28+
nullable: true

samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake-json_serializable/doc/NullableClass.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ Name | Type | Description | Notes
1515
**dateProp** | [**DateTime**](DateTime.md) | | [optional]
1616
**datetimeProp** | [**DateTime**](DateTime.md) | | [optional]
1717
**arrayNullableProp** | **List&lt;Object&gt;** | | [optional]
18-
**arrayAndItemsNullableProp** | **List&lt;Object&gt;** | | [optional]
19-
**arrayItemsNullable** | **List&lt;Object&gt;** | | [optional]
18+
**arrayAndItemsNullableProp** | **List&lt;Object?&gt;** | | [optional]
19+
**arrayItemsNullable** | **List&lt;Object?&gt;** | | [optional]
2020
**objectNullableProp** | **Map&lt;String, Object&gt;** | | [optional]
21-
**objectAndItemsNullableProp** | **Map&lt;String, Object&gt;** | | [optional]
22-
**objectItemsNullable** | **Map&lt;String, Object&gt;** | | [optional]
21+
**objectAndItemsNullableProp** | **Map&lt;String, Object?&gt;** | | [optional]
22+
**objectItemsNullable** | **Map&lt;String, Object?&gt;** | | [optional]
2323

2424
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
2525

samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake-json_serializable/lib/src/model/nullable_class.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ class NullableClass {
137137
)
138138

139139

140-
final List<Object>? arrayAndItemsNullableProp;
140+
final List<Object?>? arrayAndItemsNullableProp;
141141

142142

143143

@@ -149,7 +149,7 @@ class NullableClass {
149149
)
150150

151151

152-
final List<Object>? arrayItemsNullable;
152+
final List<Object?>? arrayItemsNullable;
153153

154154

155155

@@ -173,7 +173,7 @@ class NullableClass {
173173
)
174174

175175

176-
final Map<String, Object>? objectAndItemsNullableProp;
176+
final Map<String, Object?>? objectAndItemsNullableProp;
177177

178178

179179

@@ -185,7 +185,7 @@ class NullableClass {
185185
)
186186

187187

188-
final Map<String, Object>? objectItemsNullable;
188+
final Map<String, Object?>? objectItemsNullable;
189189

190190

191191

samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/doc/NullableClass.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ Name | Type | Description | Notes
1515
**dateProp** | [**Date**](Date.md) | | [optional]
1616
**datetimeProp** | [**DateTime**](DateTime.md) | | [optional]
1717
**arrayNullableProp** | [**BuiltList&lt;JsonObject&gt;**](JsonObject.md) | | [optional]
18-
**arrayAndItemsNullableProp** | [**BuiltList&lt;JsonObject&gt;**](JsonObject.md) | | [optional]
19-
**arrayItemsNullable** | [**BuiltList&lt;JsonObject&gt;**](JsonObject.md) | | [optional]
18+
**arrayAndItemsNullableProp** | [**BuiltList&lt;JsonObject?&gt;**](JsonObject.md) | | [optional]
19+
**arrayItemsNullable** | [**BuiltList&lt;JsonObject?&gt;**](JsonObject.md) | | [optional]
2020
**objectNullableProp** | [**BuiltMap&lt;String, JsonObject&gt;**](JsonObject.md) | | [optional]
21-
**objectAndItemsNullableProp** | [**BuiltMap&lt;String, JsonObject&gt;**](JsonObject.md) | | [optional]
22-
**objectItemsNullable** | [**BuiltMap&lt;String, JsonObject&gt;**](JsonObject.md) | | [optional]
21+
**objectAndItemsNullableProp** | [**BuiltMap&lt;String, JsonObject?&gt;**](JsonObject.md) | | [optional]
22+
**objectItemsNullable** | [**BuiltMap&lt;String, JsonObject?&gt;**](JsonObject.md) | | [optional]
2323

2424
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
2525

0 commit comments

Comments
 (0)