Skip to content

Commit e5c57b0

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

11 files changed

+154
-16
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: 4 additions & 0 deletions
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.',

json_serializable/lib/src/type_helpers/enum_helper.dart

Lines changed: 14 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,18 @@ class EnumHelper extends TypeHelper<TypeHelperContextWithConfig> {
4446
return null;
4547
}
4648

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

5468
context.addMember(memberContent);
5569

56-
final jsonKey = jsonKeyForField(context.fieldElement, context.config);
5770
final args = [
5871
constMapName(targetType),
5972
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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ const _expectedAnnotatedTests = {
9898
'NoDeserializeFieldType',
9999
'NoSerializeBadKey',
100100
'NoSerializeFieldType',
101+
'NullForUndefinedEnumValueOnNonNullableField',
101102
'ObjectConvertMethods',
102103
'OkayOneNormalOptionalNamed',
103104
'OkayOneNormalOptionalPositional',
@@ -130,6 +131,7 @@ const _expectedAnnotatedTests = {
130131
'UnsupportedSetField',
131132
'UnsupportedUriField',
132133
'ValidToFromFuncClassStatic',
134+
'WeirdValueForUnknownEnumValue',
133135
'WithANonCtorGetter',
134136
'WithANonCtorGetterChecked',
135137
'WrongConstructorNameClass',

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)