Skip to content

Commit 5107ac1

Browse files
authored
Improve encoding of values to handle null cases (#1223)
Specifically with enums and converter functions when `includeIfNull` is `false`. Enum values with `null` as an output where not properly wrapped. This is now fixed. Fixes #1202 Also 'unwrap' the cases where conversion functions never return `null`.
1 parent a79c6b7 commit 5107ac1

13 files changed

+313
-50
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.5.2
2+
3+
- Better handling of `null` when encoding `enum` values or values with
4+
conversions.
5+
16
## 6.5.1
27

38
- Fixed `BigInt`, `DateTime`, and `Uri` support for `JsonKey.defaultValue` with

json_serializable/lib/src/encoder_helper.dart

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44

55
import 'package:analyzer/dart/element/element.dart';
66
import 'package:analyzer/dart/element/nullability_suffix.dart';
7-
import 'package:json_annotation/json_annotation.dart';
87
import 'package:source_helper/source_helper.dart';
98

109
import 'constants.dart';
10+
import 'enum_utils.dart';
1111
import 'helper_core.dart';
1212
import 'type_helpers/generic_factory_helper.dart';
1313
import 'type_helpers/json_converter_helper.dart';
@@ -163,19 +163,32 @@ abstract class EncodeHelper implements HelperCore {
163163
bool _writeJsonValueNaive(FieldElement field) {
164164
final jsonKey = jsonKeyFor(field);
165165

166-
return jsonKey.includeIfNull ||
167-
(!field.type.isNullableType && !_fieldHasCustomEncoder(field));
168-
}
166+
if (jsonKey.includeIfNull) {
167+
return true;
168+
}
169169

170-
/// Returns `true` if [field] has a user-defined encoder.
171-
///
172-
/// This can be either a `toJson` function in [JsonKey] or a [JsonConverter]
173-
/// annotation.
174-
bool _fieldHasCustomEncoder(FieldElement field) {
175170
final helperContext = getHelperContext(field);
176-
return helperContext.serializeConvertData != null ||
177-
const JsonConverterHelper()
178-
.serialize(field.type, 'test', helperContext) !=
179-
null;
171+
172+
final serializeConvertData = helperContext.serializeConvertData;
173+
if (serializeConvertData != null) {
174+
return !serializeConvertData.returnType.isNullableType;
175+
}
176+
177+
final nullableEncodeConverter =
178+
hasConverterNullEncode(field.type, helperContext);
179+
180+
if (nullableEncodeConverter != null) {
181+
return !nullableEncodeConverter;
182+
}
183+
184+
// We can consider enums as kinda like having custom converters
185+
// same rules apply. If `null` is in the set of encoded values, we
186+
// should not write naive
187+
final enumWithNullValue = enumFieldWithNullInEncodeMap(field.type);
188+
if (enumWithNullValue != null) {
189+
return !enumWithNullValue;
190+
}
191+
192+
return !field.type.isNullableType;
180193
}
181194
}

json_serializable/lib/src/enum_utils.dart

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,38 @@ import 'utils.dart';
1414
String constMapName(DartType targetType) =>
1515
'_\$${targetType.element2!.name}EnumMap';
1616

17+
/// If [targetType] is not an enum, return `null`.
18+
///
19+
/// Otherwise, returns `true` if one of the encoded values of the enum is
20+
/// `null`.
21+
bool? enumFieldWithNullInEncodeMap(DartType targetType) {
22+
final enumMap = _enumMap(targetType);
23+
24+
if (enumMap == null) return null;
25+
26+
return enumMap.values.contains(null);
27+
}
28+
1729
String? enumValueMapFromType(
1830
DartType targetType, {
1931
bool nullWithNoAnnotation = false,
32+
}) {
33+
final enumMap =
34+
_enumMap(targetType, nullWithNoAnnotation: nullWithNoAnnotation);
35+
36+
if (enumMap == null) return null;
37+
38+
final items = enumMap.entries
39+
.map((e) => ' ${targetType.element2!.name}.${e.key.name}: '
40+
'${jsonLiteralAsDart(e.value)},')
41+
.join();
42+
43+
return 'const ${constMapName(targetType)} = {\n$items\n};';
44+
}
45+
46+
Map<FieldElement, Object?>? _enumMap(
47+
DartType targetType, {
48+
bool nullWithNoAnnotation = false,
2049
}) {
2150
final annotation = _jsonEnumChecker.firstAnnotationOf(targetType.element2!);
2251
final jsonEnum = _fromAnnotation(annotation);
@@ -27,21 +56,14 @@ String? enumValueMapFromType(
2756
return null;
2857
}
2958

30-
final enumMap = {
59+
return {
3160
for (var field in enumFields)
3261
field: _generateEntry(
3362
field: field,
3463
jsonEnum: jsonEnum,
3564
targetType: targetType,
3665
),
3766
};
38-
39-
final items = enumMap.entries
40-
.map((e) => ' ${targetType.element2!.name}.${e.key.name}: '
41-
'${jsonLiteralAsDart(e.value)},')
42-
.join();
43-
44-
return 'const ${constMapName(targetType)} = {\n$items\n};';
4567
}
4668

4769
Object? _generateEntry({

json_serializable/lib/src/type_helper_ctx.dart

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,13 +135,12 @@ ConvertData? _convertData(DartObject obj, FieldElement element, bool isFrom) {
135135
'positional parameter.');
136136
}
137137

138+
final returnType = executableElement.returnType;
138139
final argType = executableElement.parameters.first.type;
139140
if (isFrom) {
140141
final hasDefaultValue =
141142
!jsonKeyAnnotation(element).read('defaultValue').isNull;
142143

143-
final returnType = executableElement.returnType;
144-
145144
if (returnType is TypeParameterType) {
146145
// We keep things simple in this case. We rely on inferred type arguments
147146
// to the `fromJson` function.
@@ -176,5 +175,5 @@ ConvertData? _convertData(DartObject obj, FieldElement element, bool isFrom) {
176175
}
177176
}
178177

179-
return ConvertData(executableElement.qualifiedName, argType);
178+
return ConvertData(executableElement.qualifiedName, argType, returnType);
180179
}

json_serializable/lib/src/type_helpers/convert_helper.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ import '../type_helper.dart';
1414
class ConvertData {
1515
final String name;
1616
final DartType paramType;
17+
final DartType returnType;
1718

18-
ConvertData(this.name, this.paramType);
19+
ConvertData(this.name, this.paramType, this.returnType);
1920
}
2021

2122
abstract class TypeHelperContextWithConvert extends TypeHelperContext {

json_serializable/lib/src/type_helpers/enum_helper.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ class EnumHelper extends TypeHelper<TypeHelperContextWithConfig> {
2929

3030
context.addMember(memberContent);
3131

32-
if (targetType.isNullableType) {
32+
if (targetType.isNullableType ||
33+
enumFieldWithNullInEncodeMap(targetType) == true) {
3334
return '${constMapName(targetType)}[$expression]';
3435
} else {
3536
return '${constMapName(targetType)}[$expression]!';

json_serializable/lib/src/type_helpers/json_converter_helper.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,24 @@ class _JsonConvertData {
143143
accessor.isEmpty ? '' : '.$accessor';
144144
}
145145

146+
/// If there is no converter for the params, return `null`.
147+
///
148+
/// Otherwise, returns `true` if the converter has a null return value.
149+
///
150+
/// Used to make sure we create a smart encoding function.
151+
bool? hasConverterNullEncode(
152+
DartType targetType,
153+
TypeHelperContextWithConfig ctx,
154+
) {
155+
final data = _typeConverter(targetType, ctx);
156+
157+
if (data == null) {
158+
return null;
159+
}
160+
161+
return data.jsonType.isNullableType;
162+
}
163+
146164
_JsonConvertData? _typeConverter(
147165
DartType targetType,
148166
TypeHelperContextWithConfig ctx,

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.5.1
2+
version: 6.5.2
33
description: >-
44
Automatically generate code for converting to and from JSON by annotating
55
Dart classes.
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import 'dart:convert';
2+
3+
import 'package:json_annotation/json_annotation.dart';
4+
5+
part 'converter_examples.g.dart';
6+
7+
@JsonEnum(valueField: 'value')
8+
enum Issue1202RegressionEnum {
9+
normalValue(42),
10+
nullValue(null);
11+
12+
const Issue1202RegressionEnum(this.value);
13+
14+
final int? value;
15+
}
16+
17+
@JsonSerializable(includeIfNull: false)
18+
class Issue1202RegressionClass {
19+
@JsonKey(fromJson: _fromJson, toJson: _toJson)
20+
final int valueWithFunctions;
21+
22+
@_Issue1202RegressionNotNullConverter()
23+
final int notNullableValueWithConverter;
24+
25+
final Issue1202RegressionEnum value;
26+
final int? normalNullableValue;
27+
28+
@_Issue1202RegressionConverter()
29+
final int notNullableValueWithNullableConverter;
30+
31+
@JsonKey(fromJson: _fromJsonNullable, toJson: _toJsonNullable)
32+
final int valueWithNullableFunctions;
33+
34+
Issue1202RegressionClass({
35+
required this.value,
36+
required this.normalNullableValue,
37+
required this.notNullableValueWithNullableConverter,
38+
required this.notNullableValueWithConverter,
39+
required this.valueWithFunctions,
40+
required this.valueWithNullableFunctions,
41+
});
42+
43+
factory Issue1202RegressionClass.fromJson(Map<String, dynamic> json) =>
44+
_$Issue1202RegressionClassFromJson(json);
45+
46+
Map<String, dynamic> toJson() => _$Issue1202RegressionClassToJson(this);
47+
48+
@override
49+
bool operator ==(Object other) =>
50+
other is Issue1202RegressionClass &&
51+
jsonEncode(other) == jsonEncode(this);
52+
53+
@override
54+
int get hashCode => jsonEncode(this).hashCode;
55+
56+
static int _fromJsonNullable(String? json) {
57+
if (json == null) return _default;
58+
return int.parse(json);
59+
}
60+
61+
static String? _toJsonNullable(int object) {
62+
if (object == _default) return null;
63+
return object.toString();
64+
}
65+
66+
static int _fromJson(String json) => int.parse(json);
67+
68+
static String _toJson(int object) => object.toString();
69+
}
70+
71+
const _default = 42;
72+
73+
class _Issue1202RegressionConverter extends JsonConverter<int, String?> {
74+
const _Issue1202RegressionConverter();
75+
76+
@override
77+
int fromJson(String? json) {
78+
if (json == null) return _default;
79+
return int.parse(json);
80+
}
81+
82+
@override
83+
String? toJson(int object) {
84+
if (object == _default) return null;
85+
return object.toString();
86+
}
87+
}
88+
89+
class _Issue1202RegressionNotNullConverter extends JsonConverter<int, String> {
90+
const _Issue1202RegressionNotNullConverter();
91+
92+
@override
93+
int fromJson(String json) => int.parse(json);
94+
95+
@override
96+
String toJson(int object) => object.toString();
97+
}

json_serializable/test/integration/converter_examples.g.dart

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

0 commit comments

Comments
 (0)