Skip to content

Commit 09cca32

Browse files
authored
Support using non-nullable JsonConverter on nullable properties (#1136)
fixes #822
1 parent 5dbb1b2 commit 09cca32

18 files changed

+426
-16
lines changed

json_serializable/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 6.3.0-dev
2+
3+
- Added support for using a `JsonConverter<MyClass, Object>` on properties
4+
of type `MyClass?`. ([#822](https://github.com/google/json_serializable.dart/issues/822))
5+
16
## 6.2.0
27

38
- Added support for the new `FieldRename.screamingSnake` field in

json_serializable/lib/src/type_helpers/json_converter_helper.dart

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,24 @@ class JsonConverterHelper extends TypeHelper {
3232
return null;
3333
}
3434

35+
if (!converter.fieldType.isNullableType && targetType.isNullableType) {
36+
const converterToJsonName = r'_$JsonConverterToJson';
37+
context.addMember('''
38+
Json? $converterToJsonName<Json, Value>(
39+
Value? value,
40+
Json? Function(Value value) toJson,
41+
) => ${ifNullOrElse('value', 'null', 'toJson(value)')};
42+
''');
43+
44+
return _nullableJsonConverterLambdaResult(
45+
converter,
46+
name: converterToJsonName,
47+
targetType: targetType,
48+
expression: expression,
49+
callback: '${converter.accessString}.toJson',
50+
);
51+
}
52+
3553
return LambdaResult(expression, '${converter.accessString}.toJson');
3654
}
3755

@@ -49,6 +67,24 @@ class JsonConverterHelper extends TypeHelper {
4967

5068
final asContent = asStatement(converter.jsonType);
5169

70+
if (!converter.jsonType.isNullableType && targetType.isNullableType) {
71+
const converterFromJsonName = r'_$JsonConverterFromJson';
72+
context.addMember('''
73+
Value? $converterFromJsonName<Json, Value>(
74+
Object? json,
75+
Value? Function(Json json) fromJson,
76+
) => ${ifNullOrElse('json', 'null', 'fromJson(json as Json)')};
77+
''');
78+
79+
return _nullableJsonConverterLambdaResult(
80+
converter,
81+
name: converterFromJsonName,
82+
targetType: targetType,
83+
expression: expression,
84+
callback: '${converter.accessString}.fromJson',
85+
);
86+
}
87+
5288
return LambdaResult(
5389
expression,
5490
'${converter.accessString}.fromJson',
@@ -57,24 +93,51 @@ class JsonConverterHelper extends TypeHelper {
5793
}
5894
}
5995

96+
String _nullableJsonConverterLambdaResult(
97+
_JsonConvertData converter, {
98+
required String name,
99+
required DartType targetType,
100+
required String expression,
101+
required String callback,
102+
}) {
103+
final jsonDisplayString = typeToCode(converter.jsonType);
104+
final fieldTypeDisplayString = converter.isGeneric
105+
? typeToCode(targetType)
106+
: typeToCode(converter.fieldType);
107+
108+
return '$name<$jsonDisplayString, $fieldTypeDisplayString>('
109+
'$expression, $callback)';
110+
}
111+
60112
class _JsonConvertData {
61113
final String accessString;
62114
final DartType jsonType;
115+
final DartType fieldType;
116+
final bool isGeneric;
63117

64118
_JsonConvertData.className(
65119
String className,
66120
String accessor,
67121
this.jsonType,
68-
) : accessString = 'const $className${_withAccessor(accessor)}()';
122+
this.fieldType,
123+
) : accessString = 'const $className${_withAccessor(accessor)}()',
124+
isGeneric = false;
69125

70126
_JsonConvertData.genericClass(
71127
String className,
72128
String genericTypeArg,
73129
String accessor,
74130
this.jsonType,
75-
) : accessString = '$className<$genericTypeArg>${_withAccessor(accessor)}()';
131+
this.fieldType,
132+
) : accessString =
133+
'$className<$genericTypeArg>${_withAccessor(accessor)}()',
134+
isGeneric = true;
76135

77-
_JsonConvertData.propertyAccess(this.accessString, this.jsonType);
136+
_JsonConvertData.propertyAccess(
137+
this.accessString,
138+
this.jsonType,
139+
this.fieldType,
140+
) : isGeneric = false;
78141

79142
static String _withAccessor(String accessor) =>
80143
accessor.isEmpty ? '' : '.$accessor';
@@ -127,7 +190,11 @@ _JsonConvertData? _typeConverterFrom(
127190
accessString = '${enclosing.name}.$accessString';
128191
}
129192

130-
return _JsonConvertData.propertyAccess(accessString, match.jsonType);
193+
return _JsonConvertData.propertyAccess(
194+
accessString,
195+
match.jsonType,
196+
match.fieldType,
197+
);
131198
}
132199

133200
final reviver = ConstantReader(match.annotation).revive();
@@ -145,18 +212,21 @@ _JsonConvertData? _typeConverterFrom(
145212
match.genericTypeArg!,
146213
reviver.accessor,
147214
match.jsonType,
215+
match.fieldType,
148216
);
149217
}
150218

151219
return _JsonConvertData.className(
152220
match.annotation.type!.element!.name!,
153221
reviver.accessor,
154222
match.jsonType,
223+
match.fieldType,
155224
);
156225
}
157226

158227
class _ConverterMatch {
159228
final DartObject annotation;
229+
final DartType fieldType;
160230
final DartType jsonType;
161231
final ElementAnnotation elementAnnotation;
162232
final String? genericTypeArg;
@@ -166,6 +236,7 @@ class _ConverterMatch {
166236
this.annotation,
167237
this.jsonType,
168238
this.genericTypeArg,
239+
this.fieldType,
169240
);
170241
}
171242

@@ -191,9 +262,15 @@ _ConverterMatch? _compatibleMatch(
191262

192263
final fieldType = jsonConverterSuper.typeArguments[0];
193264

194-
if (fieldType == targetType) {
265+
// Allow assigning T to T?
266+
if (fieldType == targetType || fieldType == targetType.promoteNonNullable()) {
195267
return _ConverterMatch(
196-
annotation, constantValue, jsonConverterSuper.typeArguments[1], null);
268+
annotation,
269+
constantValue,
270+
jsonConverterSuper.typeArguments[1],
271+
null,
272+
fieldType,
273+
);
197274
}
198275

199276
if (fieldType is TypeParameterType && targetType is TypeParameterType) {
@@ -212,6 +289,7 @@ _ConverterMatch? _compatibleMatch(
212289
constantValue,
213290
jsonConverterSuper.typeArguments[1],
214291
'${targetType.element.name}${targetType.isNullableType ? '?' : ''}',
292+
fieldType,
215293
);
216294
}
217295

json_serializable/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: json_serializable
2-
version: 6.2.0
2+
version: 6.3.0-dev
33
description: >-
44
Automatically generate code for converting to and from JSON by annotating
55
Dart classes.

json_serializable/test/generic_files/generic_class.g.dart

Lines changed: 20 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

json_serializable/test/json_serializable_test.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ const _expectedAnnotatedTests = {
8585
'JsonConverterCtorParams',
8686
'JsonConverterDuplicateAnnotations',
8787
'JsonConverterNamedCtor',
88+
'JsonConverterNullableToNonNullable',
8889
'JsonConverterOnGetter',
8990
'JsonConverterWithBadTypeArg',
9091
'JsonValueValid',

json_serializable/test/kitchen_sink/kitchen_sink.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,13 @@ class _Factory implements k.KitchenSinkFactory<String, dynamic> {
6666
[],
6767
BigInt.zero,
6868
{},
69+
BigInt.zero,
70+
{},
6971
TrivialNumber(0),
7072
{},
7173
DateTime.fromMillisecondsSinceEpoch(0),
74+
TrivialNumber(0),
75+
{},
7276
);
7377

7478
k.JsonConverterTestClass jsonConverterFromJson(Map<String, dynamic> json) =>
@@ -203,9 +207,13 @@ class JsonConverterTestClass implements k.JsonConverterTestClass {
203207
this.durationList,
204208
this.bigInt,
205209
this.bigIntMap,
210+
this.nullableBigInt,
211+
this.nullableBigIntMap,
206212
this.numberSilly,
207213
this.numberSillySet,
208214
this.dateTime,
215+
this.nullableNumberSilly,
216+
this.nullableNumberSillySet,
209217
);
210218

211219
factory JsonConverterTestClass.fromJson(Map<String, dynamic> json) =>
@@ -219,10 +227,16 @@ class JsonConverterTestClass implements k.JsonConverterTestClass {
219227
BigInt bigInt;
220228
Map<String, BigInt> bigIntMap;
221229

230+
BigInt? nullableBigInt;
231+
Map<String, BigInt?> nullableBigIntMap;
232+
222233
TrivialNumber numberSilly;
223234
Set<TrivialNumber> numberSillySet;
224235

225236
DateTime? dateTime;
237+
238+
TrivialNumber? nullableNumberSilly;
239+
Set<TrivialNumber?> nullableNumberSillySet;
226240
}
227241

228242
@JsonSerializable()

json_serializable/test/kitchen_sink/kitchen_sink.g.dart

Lines changed: 37 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

json_serializable/test/kitchen_sink/kitchen_sink.g_any_map.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,13 @@ class _Factory implements k.KitchenSinkFactory<dynamic, dynamic> {
6565
[],
6666
BigInt.zero,
6767
{},
68+
BigInt.zero,
69+
{},
6870
TrivialNumber(0),
6971
{},
7072
DateTime.fromMillisecondsSinceEpoch(0),
73+
TrivialNumber(0),
74+
{},
7175
);
7276

7377
k.JsonConverterTestClass jsonConverterFromJson(Map<String, dynamic> json) =>
@@ -205,9 +209,13 @@ class JsonConverterTestClass implements k.JsonConverterTestClass {
205209
this.durationList,
206210
this.bigInt,
207211
this.bigIntMap,
212+
this.nullableBigInt,
213+
this.nullableBigIntMap,
208214
this.numberSilly,
209215
this.numberSillySet,
210216
this.dateTime,
217+
this.nullableNumberSilly,
218+
this.nullableNumberSillySet,
211219
);
212220

213221
factory JsonConverterTestClass.fromJson(Map<String, dynamic> json) =>
@@ -221,10 +229,16 @@ class JsonConverterTestClass implements k.JsonConverterTestClass {
221229
BigInt bigInt;
222230
Map<String, BigInt> bigIntMap;
223231

232+
BigInt? nullableBigInt;
233+
Map<String, BigInt?> nullableBigIntMap;
234+
224235
TrivialNumber numberSilly;
225236
Set<TrivialNumber> numberSillySet;
226237

227238
DateTime? dateTime;
239+
240+
TrivialNumber? nullableNumberSilly;
241+
Set<TrivialNumber?> nullableNumberSillySet;
228242
}
229243

230244
@JsonSerializable(

0 commit comments

Comments
 (0)