Skip to content

Commit c916df9

Browse files
authored
[dart][dart-dio] Nullable support/improvements (#8727)
* [dart-dio] Disable nullable fields by default This is not in line with the OAS and will prevent future Dart nullabilty features (NNBD) from being useful as all types would be optional. Users can still opt-in for this. * [dart-dio] Properties are nullable when not required AND not nullable * [dart][dart-dio] Support nullable/required fields * properties in built_value need to be nullable when they are nullable in OAS or when they are not required in OAS * built_value does not support serializing `null` values by default as it is based on a serialization format based on iterables/lists and not maps * dart-dio uses the built_value json plugin to convert the built_value format to regular json * by generating a custom serializer for each class we can add support for serializing `null` values if the property is required AND nullable in OAS * this is a breaking change as not all properties in the models are nullable by default anymore * Implement required/nullable for dart * Changes for set types and enum names after rebase * Add some comments and fix built_value fields with default being nullable * More rebase changes and regenerate docs
1 parent 7704ff3 commit c916df9

File tree

115 files changed

+3644
-217
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

115 files changed

+3644
-217
lines changed

docs/generators/dart-dio.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
1212
|disallowAdditionalPropertiesIfNotPresent|If false, the 'additionalProperties' implementation (set to true by default) is compliant with the OAS and JSON schema specifications. If true (default), keep the old (incorrect) behaviour that 'additionalProperties' is set to false by default.|<dl><dt>**false**</dt><dd>The 'additionalProperties' implementation is compliant with the OAS and JSON schema specifications.</dd><dt>**true**</dt><dd>Keep the old (incorrect) behaviour that 'additionalProperties' is set to false by default.</dd></dl>|true|
1313
|ensureUniqueParams|Whether to ensure parameter names are unique in an operation (rename parameters that are not).| |true|
1414
|legacyDiscriminatorBehavior|Set to true for generators with better support for discriminators. (Python, Java, Go, PowerShell, C#have this enabled by default).|<dl><dt>**true**</dt><dd>The mapping in the discriminator includes descendent schemas that allOf inherit from self and the discriminator mapping schemas in the OAS document.</dd><dt>**false**</dt><dd>The mapping in the discriminator includes any descendent schemas that allOf inherit from self, any oneOf schemas, any anyOf schemas, any x-discriminator-values, and the discriminator mapping schemas in the OAS document AND Codegen validates that oneOf and anyOf schemas contain the required discriminator and throws an error if the discriminator is missing.</dd></dl>|true|
15-
|nullableFields|Is the null fields should be in the JSON payload| |null|
15+
|nullableFields|Make all fields nullable in the JSON payload| |null|
1616
|prependFormOrBodyParameters|Add form or body parameters to the beginning of the parameter list.| |false|
1717
|pubAuthor|Author name in generated pubspec| |null|
1818
|pubAuthorEmail|Email address of the author in generated pubspec| |null|

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

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public class DartDioClientCodegen extends DartClientCodegen {
4040

4141
private static final String CLIENT_NAME = "clientName";
4242

43-
private boolean nullableFields = true;
43+
private boolean nullableFields = false;
4444
private String dateLibrary = "core";
4545

4646
public DartDioClientCodegen() {
@@ -49,7 +49,7 @@ public DartDioClientCodegen() {
4949
embeddedTemplateDir = "dart-dio";
5050
this.setTemplateDir(embeddedTemplateDir);
5151

52-
cliOptions.add(new CliOption(NULLABLE_FIELDS, "Is the null fields should be in the JSON payload"));
52+
cliOptions.add(new CliOption(NULLABLE_FIELDS, "Make all fields nullable in the JSON payload"));
5353
CliOption dateLibrary = new CliOption(DATE_LIBRARY, "Option. Date library to use").defaultValue(this.getDateLibrary());
5454
Map<String, String> dateOptions = new HashMap<>();
5555
dateOptions.put("core", "Dart core library (DateTime)");
@@ -284,6 +284,34 @@ public void postProcessModelProperty(CodegenModel model, CodegenProperty propert
284284
// enums are generated with built_value and make use of BuiltSet
285285
model.imports.add("BuiltSet");
286286
}
287+
288+
property.getVendorExtensions().put("x-built-value-serializer-type", createBuiltValueSerializerType(property));
289+
}
290+
291+
private String createBuiltValueSerializerType(CodegenProperty property) {
292+
final StringBuilder sb = new StringBuilder("const FullType(");
293+
if (property.isContainer) {
294+
appendCollection(sb, property);
295+
} else {
296+
sb.append(property.datatypeWithEnum);
297+
}
298+
sb.append(")");
299+
return sb.toString();
300+
}
301+
302+
private void appendCollection(StringBuilder sb, CodegenProperty property) {
303+
sb.append(property.baseType);
304+
sb.append(", [FullType(");
305+
if (property.isMap) {
306+
// a map always has string keys
307+
sb.append("String), FullType(");
308+
}
309+
if (property.items.isContainer) {
310+
appendCollection(sb, property.items);
311+
} else {
312+
sb.append(property.items.datatypeWithEnum);
313+
}
314+
sb.append(")]");
287315
}
288316

289317
@Override

modules/openapi-generator/src/main/resources/dart-dio/class.mustache

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,110 @@ abstract class {{classname}} implements Built<{{classname}}, {{classname}}Builde
99
{{#description}}
1010
/// {{{description}}}
1111
{{/description}}
12+
{{!
13+
A field is @nullable in built_value when it is
14+
nullable || (!required && !defaultValue) in OAS
15+
}}
1216
{{#isNullable}}
1317
@nullable
1418
{{/isNullable}}
19+
{{^isNullable}}
20+
{{^required}}
21+
{{^defaultValue}}
22+
@nullable
23+
{{/defaultValue}}
24+
{{/required}}
25+
{{/isNullable}}
1526
@BuiltValueField(wireName: r'{{baseName}}')
1627
{{{datatypeWithEnum}}} get {{name}};
1728
{{#allowableValues}}
1829
// {{#min}}range from {{{min}}} to {{{max}}}{{/min}}{{^min}}enum {{name}}Enum { {{#values}} {{{.}}}, {{/values}} };{{/min}}
1930
{{/allowableValues}}
2031

2132
{{/vars}}
22-
// Boilerplate code needed to wire-up generated code
2333
{{classname}}._();
2434

2535
static void _initializeBuilder({{{classname}}}Builder b) => b{{#vars}}{{#defaultValue}}
2636
..{{{name}}} = {{#isEnum}}{{^isContainer}}const {{{enumName}}}._({{/isContainer}}{{/isEnum}}{{{defaultValue}}}{{#isEnum}}{{^isContainer}}){{/isContainer}}{{/isEnum}}{{/defaultValue}}{{/vars}};
2737

2838
factory {{classname}}([void updates({{classname}}Builder b)]) = _${{classname}};
29-
static Serializer<{{classname}}> get serializer => _${{#lambda.camelcase}}{{{classname}}}{{/lambda.camelcase}}Serializer;
39+
40+
@BuiltValueSerializer(custom: true)
41+
static Serializer<{{classname}}> get serializer => _${{classname}}Serializer();
42+
}
43+
44+
{{!
45+
Generate a custom serializer in order to support combinations of required and nullable.
46+
By default built_value does not serialize null fields.
47+
}}
48+
class _${{classname}}Serializer implements StructuredSerializer<{{classname}}> {
49+
50+
@override
51+
final Iterable<Type> types = const [{{classname}}, _${{classname}}];
52+
@override
53+
final String wireName = r'{{classname}}';
54+
55+
@override
56+
Iterable<Object> serialize(Serializers serializers, {{{classname}}} object,
57+
{FullType specifiedType = FullType.unspecified}) {
58+
final result = <Object>[];
59+
{{#vars}}
60+
{{#required}}
61+
{{!
62+
A required property need to always be part of the serialized output.
63+
When it is nullable, null is serialized, otherwise it is an error if it is null.
64+
}}
65+
result
66+
..add(r'{{baseName}}')
67+
..add({{#isNullable}}object.{{{name}}} == null ? null : {{/isNullable}}serializers.serialize(object.{{{name}}},
68+
specifiedType: {{{vendorExtensions.x-built-value-serializer-type}}}));
69+
{{/required}}
70+
{{^required}}
71+
if (object.{{{name}}} != null) {
72+
{{! Non-required properties are only serialized if not null. }}
73+
result
74+
..add(r'{{baseName}}')
75+
..add(serializers.serialize(object.{{{name}}},
76+
specifiedType: {{{vendorExtensions.x-built-value-serializer-type}}}));
77+
}
78+
{{/required}}
79+
{{/vars}}
80+
return result;
81+
}
82+
83+
@override
84+
{{classname}} deserialize(Serializers serializers, Iterable<Object> serialized,
85+
{FullType specifiedType = FullType.unspecified}) {
86+
final result = {{classname}}Builder();
87+
88+
final iterator = serialized.iterator;
89+
while (iterator.moveNext()) {
90+
final key = iterator.current as String;
91+
iterator.moveNext();
92+
final dynamic value = iterator.current;
93+
switch (key) {
94+
{{#vars}}
95+
case r'{{baseName}}':
96+
{{#isContainer}}
97+
result.{{{name}}}.replace(serializers.deserialize(value,
98+
specifiedType: {{{vendorExtensions.x-built-value-serializer-type}}}) as {{{datatypeWithEnum}}});
99+
{{/isContainer}}
100+
{{#isModel}}
101+
result.{{{name}}}.replace(serializers.deserialize(value,
102+
specifiedType: {{{vendorExtensions.x-built-value-serializer-type}}}) as {{{datatypeWithEnum}}});
103+
{{/isModel}}
104+
{{^isContainer}}
105+
{{^isModel}}
106+
result.{{{name}}} = serializers.deserialize(value,
107+
specifiedType: {{{vendorExtensions.x-built-value-serializer-type}}}) as {{{datatypeWithEnum}}};
108+
{{/isModel}}
109+
{{/isContainer}}
110+
break;
111+
{{/vars}}
112+
}
113+
}
114+
return result.build();
115+
}
30116
}
31117
{{!
32118
Generate an enum for any variables that are declared as inline enums

modules/openapi-generator/src/main/resources/dart-dio/model_test.mustache

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import 'package:test/test.dart';
66
// tests for {{{classname}}}
77
void main() {
88
{{^isEnum}}
9-
final instance = {{{classname}}}();
9+
{{! Due to required vars without default value we can not create a full instance here }}
10+
final instance = {{{classname}}}Builder();
11+
// TODO add properties to the builder and call build()
1012
{{/isEnum}}
1113

1214
group({{{classname}}}, () {

modules/openapi-generator/src/main/resources/dart2/class.mustache

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ class {{{classname}}} {
22
/// Returns a new [{{{classname}}}] instance.
33
{{{classname}}}({
44
{{#vars}}
5-
{{#required}}{{^defaultValue}}@required {{/defaultValue}}{{/required}}this.{{{name}}}{{^isNullable}}{{#defaultValue}} = {{#isEnum}}{{^isContainer}}const {{{enumName}}}._({{/isContainer}}{{/isEnum}}{{{defaultValue}}}{{#isEnum}}{{^isContainer}}){{/isContainer}}{{/isEnum}}{{/defaultValue}}{{/isNullable}},
5+
{{!
6+
A field is @required in Dart when it is
7+
required && !nullable && !defaultValue in OAS
8+
}}
9+
{{#required}}{{^isNullable}}{{^defaultValue}}@required {{/defaultValue}}{{/isNullable}}{{/required}}this.{{{name}}}{{^isNullable}}{{#defaultValue}} = {{#isEnum}}{{^isContainer}}const {{{enumName}}}._({{/isContainer}}{{/isEnum}}{{{defaultValue}}}{{#isEnum}}{{^isContainer}}){{/isContainer}}{{/isEnum}}{{/defaultValue}}{{/isNullable}},
610
{{/vars}}
711
});
812

@@ -39,33 +43,37 @@ class {{{classname}}} {
3943
Map<String, dynamic> toJson() {
4044
final json = <String, dynamic>{};
4145
{{#vars}}
46+
{{^required}}
4247
if ({{{name}}} != null) {
48+
{{/required}}
4349
{{#isDateTime}}
4450
{{#pattern}}
45-
json[r'{{{baseName}}}'] = _dateEpochMarker == '{{{pattern}}}'
51+
json[r'{{{baseName}}}'] = {{#required}}{{#isNullable}}{{{name}}} == null ? null : {{/isNullable}}{{/required}}_dateEpochMarker == '{{{pattern}}}'
4652
? {{{name}}}.millisecondsSinceEpoch
4753
: {{{name}}}.toUtc().toIso8601String();
4854
{{/pattern}}
4955
{{^pattern}}
50-
json[r'{{{baseName}}}'] = {{{name}}}.toUtc().toIso8601String();
56+
json[r'{{{baseName}}}'] = {{#required}}{{#isNullable}}{{{name}}} == null ? null : {{/isNullable}}{{/required}}{{{name}}}.toUtc().toIso8601String();
5157
{{/pattern}}
5258
{{/isDateTime}}
5359
{{#isDate}}
5460
{{#pattern}}
55-
json[r'{{{baseName}}}'] = _dateEpochMarker == '{{{pattern}}}'
61+
json[r'{{{baseName}}}'] = {{#required}}{{#isNullable}}{{{name}}} == null ? null : {{/isNullable}}{{/required}}_dateEpochMarker == '{{{pattern}}}'
5662
? {{{name}}}.millisecondsSinceEpoch
5763
: _dateFormatter.format({{{name}}}.toUtc());
5864
{{/pattern}}
5965
{{^pattern}}
60-
json[r'{{{baseName}}}'] = _dateFormatter.format({{{name}}}.toUtc());
66+
json[r'{{{baseName}}}'] = {{#required}}{{#isNullable}}{{{name}}} == null ? null : {{/isNullable}}{{/required}}_dateFormatter.format({{{name}}}.toUtc());
6167
{{/pattern}}
6268
{{/isDate}}
6369
{{^isDateTime}}
6470
{{^isDate}}
65-
json[r'{{{baseName}}}'] = {{{name}}};
71+
json[r'{{{baseName}}}'] = {{#required}}{{#isNullable}}{{{name}}} == null ? null : {{/isNullable}}{{/required}}{{{name}}};
6672
{{/isDate}}
6773
{{/isDateTime}}
74+
{{^required}}
6875
}
76+
{{/required}}
6977
{{/vars}}
7078
return json;
7179
}

samples/client/petstore/dart-dio/petstore_client_lib/lib/model/api_response.dart

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,74 @@ abstract class ApiResponse implements Built<ApiResponse, ApiResponseBuilder> {
2424
@BuiltValueField(wireName: r'message')
2525
String get message;
2626

27-
// Boilerplate code needed to wire-up generated code
2827
ApiResponse._();
2928

3029
static void _initializeBuilder(ApiResponseBuilder b) => b;
3130

3231
factory ApiResponse([void updates(ApiResponseBuilder b)]) = _$ApiResponse;
33-
static Serializer<ApiResponse> get serializer => _$apiResponseSerializer;
32+
33+
@BuiltValueSerializer(custom: true)
34+
static Serializer<ApiResponse> get serializer => _$ApiResponseSerializer();
35+
}
36+
37+
class _$ApiResponseSerializer implements StructuredSerializer<ApiResponse> {
38+
39+
@override
40+
final Iterable<Type> types = const [ApiResponse, _$ApiResponse];
41+
@override
42+
final String wireName = r'ApiResponse';
43+
44+
@override
45+
Iterable<Object> serialize(Serializers serializers, ApiResponse object,
46+
{FullType specifiedType = FullType.unspecified}) {
47+
final result = <Object>[];
48+
if (object.code != null) {
49+
result
50+
..add(r'code')
51+
..add(serializers.serialize(object.code,
52+
specifiedType: const FullType(int)));
53+
}
54+
if (object.type != null) {
55+
result
56+
..add(r'type')
57+
..add(serializers.serialize(object.type,
58+
specifiedType: const FullType(String)));
59+
}
60+
if (object.message != null) {
61+
result
62+
..add(r'message')
63+
..add(serializers.serialize(object.message,
64+
specifiedType: const FullType(String)));
65+
}
66+
return result;
67+
}
68+
69+
@override
70+
ApiResponse deserialize(Serializers serializers, Iterable<Object> serialized,
71+
{FullType specifiedType = FullType.unspecified}) {
72+
final result = ApiResponseBuilder();
73+
74+
final iterator = serialized.iterator;
75+
while (iterator.moveNext()) {
76+
final key = iterator.current as String;
77+
iterator.moveNext();
78+
final dynamic value = iterator.current;
79+
switch (key) {
80+
case r'code':
81+
result.code = serializers.deserialize(value,
82+
specifiedType: const FullType(int)) as int;
83+
break;
84+
case r'type':
85+
result.type = serializers.deserialize(value,
86+
specifiedType: const FullType(String)) as String;
87+
break;
88+
case r'message':
89+
result.message = serializers.deserialize(value,
90+
specifiedType: const FullType(String)) as String;
91+
break;
92+
}
93+
}
94+
return result.build();
95+
}
3496
}
3597

samples/client/petstore/dart-dio/petstore_client_lib/lib/model/category.dart

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,64 @@ abstract class Category implements Built<Category, CategoryBuilder> {
2020
@BuiltValueField(wireName: r'name')
2121
String get name;
2222

23-
// Boilerplate code needed to wire-up generated code
2423
Category._();
2524

2625
static void _initializeBuilder(CategoryBuilder b) => b;
2726

2827
factory Category([void updates(CategoryBuilder b)]) = _$Category;
29-
static Serializer<Category> get serializer => _$categorySerializer;
28+
29+
@BuiltValueSerializer(custom: true)
30+
static Serializer<Category> get serializer => _$CategorySerializer();
31+
}
32+
33+
class _$CategorySerializer implements StructuredSerializer<Category> {
34+
35+
@override
36+
final Iterable<Type> types = const [Category, _$Category];
37+
@override
38+
final String wireName = r'Category';
39+
40+
@override
41+
Iterable<Object> serialize(Serializers serializers, Category object,
42+
{FullType specifiedType = FullType.unspecified}) {
43+
final result = <Object>[];
44+
if (object.id != null) {
45+
result
46+
..add(r'id')
47+
..add(serializers.serialize(object.id,
48+
specifiedType: const FullType(int)));
49+
}
50+
if (object.name != null) {
51+
result
52+
..add(r'name')
53+
..add(serializers.serialize(object.name,
54+
specifiedType: const FullType(String)));
55+
}
56+
return result;
57+
}
58+
59+
@override
60+
Category deserialize(Serializers serializers, Iterable<Object> serialized,
61+
{FullType specifiedType = FullType.unspecified}) {
62+
final result = CategoryBuilder();
63+
64+
final iterator = serialized.iterator;
65+
while (iterator.moveNext()) {
66+
final key = iterator.current as String;
67+
iterator.moveNext();
68+
final dynamic value = iterator.current;
69+
switch (key) {
70+
case r'id':
71+
result.id = serializers.deserialize(value,
72+
specifiedType: const FullType(int)) as int;
73+
break;
74+
case r'name':
75+
result.name = serializers.deserialize(value,
76+
specifiedType: const FullType(String)) as String;
77+
break;
78+
}
79+
}
80+
return result.build();
81+
}
3082
}
3183

0 commit comments

Comments
 (0)