Skip to content

Commit b15f0d0

Browse files
committed
Add sentinel value for null as the fallback for an unknown enum value
Fixes #559
1 parent dabcecb commit b15f0d0

13 files changed

+178
-18
lines changed

json_annotation/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
- Added `JsonEnum` for annotating `enum` types.
66
- Added `$enumDecodeNullable` and `$enumDecode` helpers to minimize generated
77
code.
8+
- Added `const` `JsonKey.nullForUndefinedEnumValue` for use in
9+
`JsonKey.unknownEnumValue` when you want to use `null` for an unknown value.
810
- Require Dart SDK `>=2.14.0`.
911

1012
## 4.1.0

json_annotation/lib/src/enum_helpers.dart

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
import 'json_key.dart';
6+
57
/// Returns the key associated with value [source] from [enumValues], if one
68
/// exists.
79
///
@@ -14,13 +16,31 @@
1416
/// Not meant to be used directly by user code.
1517
K? $enumDecodeNullable<K extends Enum, V>(
1618
Map<K, V> enumValues,
17-
dynamic source, {
18-
K? unknownValue,
19+
Object? source, {
20+
Object? unknownValue,
1921
}) {
2022
if (source == null) {
2123
return null;
2224
}
23-
return $enumDecode<K, V>(enumValues, source, unknownValue: unknownValue);
25+
26+
for (var entry in enumValues.entries) {
27+
if (entry.value == source) {
28+
return entry.key;
29+
}
30+
}
31+
32+
if (unknownValue == JsonKey.nullForUndefinedEnumValue) {
33+
return null;
34+
}
35+
36+
if (unknownValue == null) {
37+
throw ArgumentError(
38+
'`$source` is not one of the supported values: '
39+
'${enumValues.values.join(', ')}',
40+
);
41+
}
42+
43+
return unknownValue as K;
2444
}
2545

2646
/// Returns the key associated with value [source] from [enumValues], if one
@@ -45,16 +65,18 @@ K $enumDecode<K extends Enum, V>(
4565
);
4666
}
4767

48-
return enumValues.entries.singleWhere(
49-
(e) => e.value == source,
50-
orElse: () {
51-
if (unknownValue == null) {
52-
throw ArgumentError(
53-
'`$source` is not one of the supported values: '
54-
'${enumValues.values.join(', ')}',
55-
);
56-
}
57-
return MapEntry(unknownValue, enumValues.values.first);
58-
},
59-
).key;
68+
for (var entry in enumValues.entries) {
69+
if (entry.value == source) {
70+
return entry.key;
71+
}
72+
}
73+
74+
if (unknownValue == null) {
75+
throw ArgumentError(
76+
'`$source` is not one of the supported values: '
77+
'${enumValues.values.join(', ')}',
78+
);
79+
}
80+
81+
return unknownValue;
6082
}

json_annotation/lib/src/json_key.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ class JsonKey {
9191
/// source enum.
9292
///
9393
/// Valid only on enum fields with a compatible enum value.
94+
///
95+
/// If you want to use the value `null` when encountering an unknown value,
96+
/// use the value of [JsonKey.nullForUndefinedEnumValue] instead. This is only
97+
/// valid on an nullable enum field.
9498
final Object? unknownEnumValue;
9599

96100
/// Creates a new [JsonKey] instance.
@@ -108,4 +112,10 @@ class JsonKey {
108112
this.toJson,
109113
this.unknownEnumValue,
110114
});
115+
116+
/// Sentinel value for use with [unknownEnumValue].
117+
///
118+
/// Read the documentation on [unknownEnumValue] for more details.
119+
static const Object nullForUndefinedEnumValue =
120+
r'JsonKey.nullForUndefinedEnumValue';
111121
}

json_serializable/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
- Added support for `JsonSerializabel.constructor` to allow specifying an
44
alternative constructor to invoke when creating a `fromJson` helper.
55
- Support the new `@JsonEnum` annotation in `package:json_annotation`.
6+
- Support `JsonKey.nullForUndefinedEnumValue` as a value for
7+
`JsonKey.unknownEnumValue` when you want to use `null` as the unknown value.
68
- Use the new `$enumDecodeNullable` and `$enumDecode` in `json_annotation'
79
instead of generating these for each library.
810
**NOTE**: This is a potential breaking change if any user code relies on

json_serializable/lib/src/json_key_utils.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:analyzer/dart/constant/value.dart';
66
import 'package:analyzer/dart/element/element.dart';
77
import 'package:analyzer/dart/element/type.dart';
88
import 'package:build/build.dart';
9+
import 'package:json_annotation/json_annotation.dart';
910
import 'package:source_gen/source_gen.dart';
1011
import 'package:source_helper/source_helper.dart';
1112

@@ -171,6 +172,9 @@ KeyConfig _from(FieldElement element, ClassConfig classAnnotation) {
171172
return null;
172173
}
173174
if (mustBeEnum) {
175+
if (defaultValueLiteral == JsonKey.nullForUndefinedEnumValue) {
176+
return defaultValueLiteral as String;
177+
}
174178
throwUnsupported(
175179
element,
176180
'The value provided for `$fieldName` must be a matching enum.',
@@ -212,7 +216,7 @@ KeyConfig _populateJsonKey(
212216
bool? includeIfNull,
213217
String? name,
214218
bool? required,
215-
Object? unknownEnumValue,
219+
String? unknownEnumValue,
216220
}) {
217221
if (disallowNullValue == true) {
218222
if (includeIfNull == true) {

json_serializable/lib/src/type_helpers/config_types.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class KeyConfig {
1818

1919
final bool required;
2020

21-
final Object? unknownEnumValue;
21+
final String? unknownEnumValue;
2222

2323
KeyConfig({
2424
required this.defaultValue,

json_serializable/lib/src/type_helpers/enum_helper.dart

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
import 'package:analyzer/dart/element/type.dart';
6+
import 'package:json_annotation/json_annotation.dart';
7+
import 'package:source_gen/source_gen.dart';
68
import 'package:source_helper/source_helper.dart';
79

810
import '../enum_utils.dart';
@@ -44,6 +46,26 @@ class EnumHelper extends TypeHelper<TypeHelperContextWithConfig> {
4446
return null;
4547
}
4648

49+
final jsonKey = jsonKeyForField(context.fieldElement, context.config);
50+
51+
if (jsonKey.defaultValue == "'${JsonKey.nullForUndefinedEnumValue}'") {
52+
throw InvalidGenerationSourceError(
53+
'`${JsonKey.nullForUndefinedEnumValue}` cannot be used with '
54+
'`JsonKey.defaultValue`.',
55+
element: context.fieldElement,
56+
);
57+
}
58+
59+
if (!targetType.isNullableType &&
60+
jsonKey.unknownEnumValue == JsonKey.nullForUndefinedEnumValue) {
61+
// If the target is not nullable,
62+
throw InvalidGenerationSourceError(
63+
'`${JsonKey.nullForUndefinedEnumValue}` cannot be used with '
64+
'`JsonKey.unknownEnumValue` unless the field is nullable.',
65+
element: context.fieldElement,
66+
);
67+
}
68+
4769
String functionName;
4870
if (targetType.isNullableType || defaultProvided) {
4971
functionName = r'$enumDecodeNullable';
@@ -53,7 +75,6 @@ class EnumHelper extends TypeHelper<TypeHelperContextWithConfig> {
5375

5476
context.addMember(memberContent);
5577

56-
final jsonKey = jsonKeyForField(context.fieldElement, context.config);
5778
final args = [
5879
constMapName(targetType),
5980
expression,

json_serializable/test/integration/integration_test.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
import 'package:json_annotation/json_annotation.dart';
56
import 'package:test/test.dart';
67

78
import '../test_utils.dart';
@@ -302,4 +303,23 @@ void main() {
302303
expect(standAloneEnumValues, ['a', 'b', 'g', 'd']);
303304
expect(dayTypeEnumValues, ['no-good', 'rotten', 'very-bad']);
304305
});
306+
307+
test('unknown as null for enum', () {
308+
expect(
309+
() => Issue559Regression.fromJson({}).status,
310+
throwsA(isA<MissingRequiredKeysException>()),
311+
);
312+
expect(
313+
() => Issue559Regression.fromJson({'status': null}).status,
314+
throwsA(isA<DisallowedNullValueException>()),
315+
);
316+
expect(
317+
Issue559Regression.fromJson({'status': 'gamma'}).status,
318+
Issue559RegressionEnum.gamma,
319+
);
320+
expect(
321+
Issue559Regression.fromJson({'status': 'bob'}).status,
322+
isNull,
323+
);
324+
});
305325
}

json_serializable/test/integration/json_enum_example.dart

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,28 @@ enum DayType {
2424
}
2525

2626
Iterable<String> get dayTypeEnumValues => _$DayTypeEnumMap.values;
27+
28+
@JsonSerializable(
29+
createToJson: false,
30+
)
31+
class Issue559Regression {
32+
Issue559Regression({
33+
required this.status,
34+
});
35+
36+
factory Issue559Regression.fromJson(Map<String, dynamic> json) =>
37+
_$Issue559RegressionFromJson(json);
38+
39+
@JsonKey(
40+
disallowNullValue: true,
41+
required: true,
42+
unknownEnumValue: JsonKey.nullForUndefinedEnumValue,
43+
)
44+
final Issue559RegressionEnum? status;
45+
}
46+
47+
enum Issue559RegressionEnum {
48+
alpha,
49+
beta,
50+
gamma,
51+
}

json_serializable/test/integration/json_enum_example.g.dart

Lines changed: 18 additions & 0 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: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const _expectedAnnotatedTests = {
4141
'BadOneNamed',
4242
'BadToFuncReturnType',
4343
'BadTwoRequiredPositional',
44+
'BadEnumDefaultValue',
4445
'_BetterPrivateNames',
4546
'CtorDefaultValueAndJsonKeyDefaultValue',
4647
'DefaultDoubleConstants',
@@ -98,6 +99,7 @@ const _expectedAnnotatedTests = {
9899
'NoDeserializeFieldType',
99100
'NoSerializeBadKey',
100101
'NoSerializeFieldType',
102+
'NullForUndefinedEnumValueOnNonNullableField',
101103
'ObjectConvertMethods',
102104
'OkayOneNormalOptionalNamed',
103105
'OkayOneNormalOptionalPositional',
@@ -130,6 +132,7 @@ const _expectedAnnotatedTests = {
130132
'UnsupportedSetField',
131133
'UnsupportedUriField',
132134
'ValidToFromFuncClassStatic',
135+
'WeirdValueForUnknownEnumValue',
133136
'WithANonCtorGetter',
134137
'WithANonCtorGetterChecked',
135138
'WrongConstructorNameClass',

json_serializable/test/src/default_value_input.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,19 @@ class DefaultWithNestedEnum {
7373
DefaultWithNestedEnum();
7474
}
7575

76+
@ShouldThrow(
77+
'`JsonKey.nullForUndefinedEnumValue` cannot be used with '
78+
'`JsonKey.defaultValue`.',
79+
element: 'enumValue',
80+
)
81+
@JsonSerializable()
82+
class BadEnumDefaultValue {
83+
@JsonKey(defaultValue: JsonKey.nullForUndefinedEnumValue)
84+
Enum? enumValue;
85+
86+
BadEnumDefaultValue();
87+
}
88+
7689
@ShouldGenerate(
7790
r'''
7891
DefaultWithToJsonClass _$DefaultWithToJsonClassFromJson(

json_serializable/test/src/unknown_enum_value_test_input.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,23 @@ class UnknownEnumValueNotEnumField {
8686
@JsonKey(unknownEnumValue: UnknownEnumValueItems.vUnknown)
8787
int? value;
8888
}
89+
90+
@ShouldThrow(
91+
'`JsonKey.nullForUndefinedEnumValue` cannot be used with '
92+
'`JsonKey.unknownEnumValue` unless the field is nullable.',
93+
)
94+
@JsonSerializable()
95+
class NullForUndefinedEnumValueOnNonNullableField {
96+
@JsonKey(unknownEnumValue: JsonKey.nullForUndefinedEnumValue)
97+
late UnknownEnumValueItems value;
98+
}
99+
100+
@ShouldThrow(
101+
'Error with `@JsonKey` on the `value` field. `unknownEnumValue` is '
102+
'`JsonSerializable`, it must be a literal.',
103+
)
104+
@JsonSerializable()
105+
class WeirdValueForUnknownEnumValue {
106+
@JsonKey(unknownEnumValue: JsonSerializable())
107+
late UnknownEnumValueItems value;
108+
}

0 commit comments

Comments
 (0)