Skip to content

Commit 93f8da4

Browse files
authored
Handle the intersection of convert function, !nullable & !includeIfNull (#443)
If a field has a conversion function defined – either `JsonKey.toJson` or a custom `JsonConverter` annotation – handle the case where the function returns `null` and both `nullable` and `includeIfNull` are `false`. Before this change, you could still get `null` values in output JSON even if `includeIfNull` is `false`. Prepare to release v2.2.0
1 parent 15aeb4c commit 93f8da4

11 files changed

+292
-49
lines changed

json_serializable/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## 2.2.0
2+
3+
- If a field has a conversion function defined – either `JsonKey.toJson` or a
4+
custom `JsonConverter` annotation – handle the case where the function
5+
returns `null` and both `nullable` and `includeIfNull` are `false`.
6+
17
## 2.1.2
28

39
* Support `package:json_annotation` `>=2.1.0 <2.3.0`.

json_serializable/lib/src/encoder_helper.dart

Lines changed: 87 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import 'package:json_annotation/json_annotation.dart';
88
import 'constants.dart';
99
import 'helper_core.dart';
1010
import 'type_helper.dart';
11+
import 'type_helpers/convert_helper.dart';
12+
import 'type_helpers/json_converter_helper.dart';
1113

1214
abstract class EncodeHelper implements HelperCore {
1315
String _fieldAccess(FieldElement field) {
@@ -110,7 +112,8 @@ class ${_wrapperClassName(true)} extends \$JsonMapWrapper {
110112
String check;
111113

112114
if (!_writeJsonValueNaive(field)) {
113-
check = '_v.${field.name} != null';
115+
final expression = _wrapCustomEncoder('_v.${field.name}', field);
116+
check = '$expression != null';
114117

115118
if (!jsonKeyFor(field).encodeEmptyCollection) {
116119
assert(!jsonKeyFor(field).includeIfNull);
@@ -248,12 +251,88 @@ class ${_wrapperClassName(true)} extends \$JsonMapWrapper {
248251

249252
/// Returns `true` if the field can be written to JSON 'naively' – meaning
250253
/// we can avoid checking for `null`.
254+
bool _writeJsonValueNaive(FieldElement field) {
255+
final jsonKey = jsonKeyFor(field);
256+
257+
if (jsonKey.includeIfNull) {
258+
return true;
259+
}
260+
261+
if (!jsonKey.nullable &&
262+
jsonKey.encodeEmptyCollection &&
263+
!_fieldHasCustomEncoder(field)) {
264+
return true;
265+
}
266+
267+
return false;
268+
}
269+
270+
/// Returns `true` if [field] has a user-defined encoder.
251271
///
252-
/// `true` if either:
253-
/// `includeIfNull` is `true`
254-
/// or
255-
/// `nullable` is `false` and `encodeEmptyCollection` is true
256-
bool _writeJsonValueNaive(FieldElement field) =>
257-
jsonKeyFor(field).includeIfNull ||
258-
(!jsonKeyFor(field).nullable && jsonKeyFor(field).encodeEmptyCollection);
272+
/// This can be either a `toJson` function in [JsonKey] or a [JsonConverter]
273+
/// annotation.
274+
bool _fieldHasCustomEncoder(FieldElement field) {
275+
final helperContext = getHelperContext(field);
276+
277+
if (helperContext.serializeConvertData != null) {
278+
return true;
279+
}
280+
281+
final output = const JsonConverterHelper()
282+
.serialize(field.type, 'test', helperContext);
283+
284+
if (output != null) {
285+
return true;
286+
}
287+
return false;
288+
}
289+
290+
/// If [field] has a user-defined encoder, return [expression] wrapped in
291+
/// the corresponding conversion logic so we can do a correct `null` check.
292+
///
293+
/// This can be either a `toJson` function in [JsonKey] or a [JsonConverter]
294+
/// annotation.
295+
///
296+
/// If there is no user-defined encoder, just return [expression] as-is.
297+
String _wrapCustomEncoder(String expression, FieldElement field) {
298+
final helperContext = getHelperContext(field);
299+
300+
final convertData = helperContext.serializeConvertData;
301+
302+
var result = expression;
303+
if (convertData != null) {
304+
result = toJsonSerializeImpl(
305+
getHelperContext(field).serializeConvertData.name,
306+
expression,
307+
jsonKeyFor(field).nullable,
308+
);
309+
} else {
310+
final output = const JsonConverterHelper()
311+
.serialize(field.type, expression, helperContext);
312+
313+
if (output != null) {
314+
result = output.toString();
315+
}
316+
}
317+
318+
assert(
319+
(result != expression) == _fieldHasCustomEncoder(field),
320+
'If the output expression is different, then it should map to a field '
321+
'with a custom encoder',
322+
);
323+
324+
if (result == expression) {
325+
// No conversion
326+
return expression;
327+
}
328+
329+
if (jsonKeyFor(field).nullable) {
330+
// If there was a conversion and the field is nullable, wrap the output
331+
// in () – there will be null checks that will break the comparison
332+
// in the caller
333+
result = '($result)';
334+
}
335+
336+
return result;
337+
}
259338
}

json_serializable/lib/src/type_helpers/convert_helper.dart

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@ class ConvertHelper extends TypeHelper<TypeHelperContextWithConvert> {
3131
if (toJsonData != null) {
3232
assert(toJsonData.paramType is TypeParameterType ||
3333
targetType.isAssignableTo(toJsonData.paramType));
34-
final result = '${toJsonData.name}($expression)';
35-
return commonNullPrefix(context.nullable, expression, result).toString();
34+
return toJsonSerializeImpl(toJsonData.name, expression, context.nullable);
3635
}
3736
return null;
3837
}
@@ -49,3 +48,10 @@ class ConvertHelper extends TypeHelper<TypeHelperContextWithConvert> {
4948
return null;
5049
}
5150
}
51+
52+
/// Exposed to support `EncodeHelper` – not exposed publicly.
53+
String toJsonSerializeImpl(
54+
String toJsonDataName, String expression, bool nullable) {
55+
final result = '$toJsonDataName($expression)';
56+
return commonNullPrefix(nullable, expression, result).toString();
57+
}

json_serializable/lib/type_helper.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export 'src/shared_checkers.dart' show simpleJsonTypeChecker, typeArgumentsOf;
66
export 'src/type_helper.dart'
77
show TypeHelperContext, TypeHelper, UnsupportedTypeError;
88
export 'src/type_helpers/big_int_helper.dart';
9-
export 'src/type_helpers/convert_helper.dart';
9+
export 'src/type_helpers/convert_helper.dart' hide toJsonSerializeImpl;
1010
export 'src/type_helpers/date_time_helper.dart';
1111
export 'src/type_helpers/enum_helper.dart';
1212
export 'src/type_helpers/iterable_helper.dart';

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: 2.1.2
2+
version: 2.2.0
33
author: Dart Team <misc@dartlang.org>
44
description: >-
55
Automatically generate code for converting to and from JSON by annotating

json_serializable/test/json_serializable_test.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ const _expectedAnnotatedTests = [
100100
'SubTypeWithAnnotatedFieldOverrideExtendsWithOverrides',
101101
'SubTypeWithAnnotatedFieldOverrideImplements',
102102
'theAnswer',
103+
'ToJsonIncludeIfNullFalseWrapped',
104+
'ToJsonNullableFalseIncludeIfNullFalse',
105+
'ToJsonNullableFalseIncludeIfNullFalseWrapped',
103106
'TypedConvertMethods',
104107
'UnknownCtorParamType',
105108
'UnknownFieldType',

json_serializable/test/kitchen_sink/kitchen_sink.g_exclude_null__no_encode_empty__non_nullable__use_wrappers.g.dart

Lines changed: 15 additions & 5 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_exclude_null__no_encode_empty__use_wrappers.g.dart

Lines changed: 16 additions & 5 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_exclude_null__non_nullable.g.dart

Lines changed: 40 additions & 22 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_exclude_null__use_wrappers.g.dart

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

0 commit comments

Comments
 (0)