diff --git a/json_annotation/lib/src/enum_helpers.dart b/json_annotation/lib/src/enum_helpers.dart index c0c5cb3ed..4c862d52c 100644 --- a/json_annotation/lib/src/enum_helpers.dart +++ b/json_annotation/lib/src/enum_helpers.dart @@ -4,6 +4,21 @@ import 'json_key.dart'; +/// Compare an enum value against a source using case-insensitive. +/// +/// Exposed only for code generated by `package:json_serializable`. +/// Not meant to be used directly by user code. +bool $enumCompareCaseInsensitive(V arg1, Object arg2) => + ((arg1 is String) && (arg2 is String)) + ? (arg1.toLowerCase() == arg2.toLowerCase()) + : arg1 == arg2; + +/// Compare an enum value against a source. +/// +/// Exposed only for code generated by `package:json_serializable`. +/// Not meant to be used directly by user code. +bool $enumCompareStandard(V arg1, Object arg2) => arg1 == arg2; + /// Returns the key associated with value [source] from [enumValues], if one /// exists. /// @@ -18,13 +33,16 @@ K? $enumDecodeNullable( Map enumValues, Object? source, { Enum? unknownValue, + bool Function(V arg1, Object arg2)? comparator, }) { if (source == null) { return null; } + comparator ??= $enumCompareStandard; + for (var entry in enumValues.entries) { - if (entry.value == source) { + if (comparator(entry.value, source)) { return entry.key; } } @@ -65,6 +83,7 @@ K $enumDecode( Map enumValues, Object? source, { K? unknownValue, + bool Function(V arg1, Object arg2)? comparator, }) { if (source == null) { throw ArgumentError( @@ -73,8 +92,9 @@ K $enumDecode( ); } + comparator ??= $enumCompareStandard; for (var entry in enumValues.entries) { - if (entry.value == source) { + if (comparator(entry.value, source)) { return entry.key; } } diff --git a/json_annotation/lib/src/json_enum.dart b/json_annotation/lib/src/json_enum.dart index 27d1fb5bc..e05e69cea 100644 --- a/json_annotation/lib/src/json_enum.dart +++ b/json_annotation/lib/src/json_enum.dart @@ -13,6 +13,7 @@ class JsonEnum { const JsonEnum({ this.alwaysCreate = false, this.fieldRename = FieldRename.none, + this.caseInsensitive = false, this.valueField, }); @@ -36,6 +37,12 @@ class JsonEnum { /// for entries annotated with [JsonValue]. final FieldRename fieldRename; + /// If `true`, enum comparison will be done using case-insensitive. + /// + /// The default, `false`, means enum comparison will be done using + /// case-sensitive. + final bool caseInsensitive; + /// Specifies the field within an "enhanced enum" to use as the value /// to use for serialization. /// diff --git a/json_annotation/lib/src/json_key.dart b/json_annotation/lib/src/json_key.dart index d7c97b65b..2e281bc49 100644 --- a/json_annotation/lib/src/json_key.dart +++ b/json_annotation/lib/src/json_key.dart @@ -149,11 +149,18 @@ class JsonKey { /// valid on a nullable enum field. final Enum? unknownEnumValue; + /// If true, enum will be parsed with case-insensitive. + /// Specifically, both values will be lower-cased and compared. + /// + /// Valid only on enum fields with a compatible enum value. + final bool caseInsensitive; + /// Creates a new [JsonKey] instance. /// /// Only required when the default behavior is not desired. const JsonKey({ - @Deprecated('Has no effect') bool? nullable, + @Deprecated('Has no effect') + bool? nullable, this.defaultValue, this.disallowNullValue, this.fromJson, @@ -161,7 +168,7 @@ class JsonKey { 'Use `includeFromJson` and `includeToJson` with a value of `false` ' 'instead.', ) - this.ignore, + this.ignore, this.includeFromJson, this.includeIfNull, this.includeToJson, @@ -170,6 +177,7 @@ class JsonKey { this.required, this.toJson, this.unknownEnumValue, + this.caseInsensitive = false, }); /// Sentinel value for use with [unknownEnumValue]. diff --git a/json_serializable/lib/src/enum_utils.dart b/json_serializable/lib/src/enum_utils.dart index d5aa4d2d2..c285da320 100644 --- a/json_serializable/lib/src/enum_utils.dart +++ b/json_serializable/lib/src/enum_utils.dart @@ -147,7 +147,11 @@ JsonEnum _fromAnnotation(DartObject? dartObject) { final reader = ConstantReader(dartObject); return JsonEnum( alwaysCreate: reader.read('alwaysCreate').literalValue as bool, - fieldRename: readEnum(reader.read('fieldRename'), FieldRename.values)!, + fieldRename: enumValueForDartObject( + reader.read('fieldRename').objectValue, + FieldRename.values, + (f) => f.toString().split('.')[1], + ), valueField: reader.read('valueField').literalValue as String?, ); } diff --git a/json_serializable/lib/src/json_key_utils.dart b/json_serializable/lib/src/json_key_utils.dart index 42eb0cfce..26f31e5ad 100644 --- a/json_serializable/lib/src/json_key_utils.dart +++ b/json_serializable/lib/src/json_key_utils.dart @@ -30,10 +30,15 @@ KeyConfig _from(FieldElement element, ClassConfig classAnnotation) { final ctorParamDefault = classAnnotation.ctorParamDefaults[element.name]; if (obj.isNull) { + final enumObj = jsonEnumAnnotation(element); + return _populateJsonKey( classAnnotation, element, defaultValue: ctorParamDefault, + caseInsensitive: enumObj.isNull + ? null + : enumObj.read('caseInsensitive').literalValue as bool?, includeFromJson: classAnnotation.ignoreUnannotated ? false : null, includeToJson: classAnnotation.ignoreUnannotated ? false : null, ); @@ -273,6 +278,7 @@ KeyConfig _from(FieldElement element, ClassConfig classAnnotation) { createAnnotationValue('unknownEnumValue', mustBeEnum: true), includeToJson: includeToJson, includeFromJson: includeFromJson, + caseInsensitive: obj.read('caseInsensitive').literalValue as bool?, ); } @@ -286,6 +292,7 @@ KeyConfig _populateJsonKey( String? readValueFunctionName, bool? required, String? unknownEnumValue, + bool? caseInsensitive, bool? includeToJson, bool? includeFromJson, }) { @@ -307,6 +314,7 @@ KeyConfig _populateJsonKey( readValueFunctionName: readValueFunctionName, required: required ?? false, unknownEnumValue: unknownEnumValue, + caseInsensitive: caseInsensitive, includeFromJson: includeFromJson, includeToJson: includeToJson, ); diff --git a/json_serializable/lib/src/type_helpers/config_types.dart b/json_serializable/lib/src/type_helpers/config_types.dart index c4bdc260a..a8af7b73d 100644 --- a/json_serializable/lib/src/type_helpers/config_types.dart +++ b/json_serializable/lib/src/type_helpers/config_types.dart @@ -23,6 +23,8 @@ class KeyConfig { final String? unknownEnumValue; + final bool? caseInsensitive; + final String? readValueFunctionName; KeyConfig({ @@ -35,6 +37,7 @@ class KeyConfig { required this.readValueFunctionName, required this.required, required this.unknownEnumValue, + required this.caseInsensitive, }); } diff --git a/json_serializable/lib/src/type_helpers/enum_helper.dart b/json_serializable/lib/src/type_helpers/enum_helper.dart index ce7641fdf..b5e02854a 100644 --- a/json_serializable/lib/src/type_helpers/enum_helper.dart +++ b/json_serializable/lib/src/type_helpers/enum_helper.dart @@ -76,6 +76,8 @@ class EnumHelper extends TypeHelper { expression, if (jsonKey.unknownEnumValue != null) 'unknownValue: ${jsonKey.unknownEnumValue}', + if ((jsonKey.caseInsensitive ?? false) == true) + r'comparator: $enumCompareCaseInsensitive', ]; return '$functionName(${args.join(', ')})'; diff --git a/json_serializable/lib/src/utils.dart b/json_serializable/lib/src/utils.dart index 8163247c1..50fbf732c 100644 --- a/json_serializable/lib/src/utils.dart +++ b/json_serializable/lib/src/utils.dart @@ -13,6 +13,8 @@ import 'type_helpers/config_types.dart'; const _jsonKeyChecker = TypeChecker.fromRuntime(JsonKey); +const _jsonEnumChecker = TypeChecker.fromRuntime(JsonEnum); + DartObject? _jsonKeyAnnotation(FieldElement element) => _jsonKeyChecker.firstAnnotationOf(element) ?? (element.getter == null @@ -22,6 +24,14 @@ DartObject? _jsonKeyAnnotation(FieldElement element) => ConstantReader jsonKeyAnnotation(FieldElement element) => ConstantReader(_jsonKeyAnnotation(element)); +DartObject? _jsonEnumAnnotation(Element? element) => + (element != null && element is EnumElement) + ? _jsonEnumChecker.firstAnnotationOf(element) + : null; + +ConstantReader jsonEnumAnnotation(FieldElement element) => + ConstantReader(_jsonEnumAnnotation(element.type.element)); + /// Returns `true` if [element] is annotated with [JsonKey]. bool hasJsonKeyAnnotation(FieldElement element) => _jsonKeyAnnotation(element) != null; diff --git a/json_serializable/test/integration/integration_test.dart b/json_serializable/test/integration/integration_test.dart index 76b01f8fa..63e8a3232 100644 --- a/json_serializable/test/integration/integration_test.dart +++ b/json_serializable/test/integration/integration_test.dart @@ -10,7 +10,7 @@ import 'converter_examples.dart'; import 'create_per_field_to_json_example.dart'; import 'field_map_example.dart'; import 'json_enum_example.dart'; -import 'json_test_common.dart' show Category, Platform, StatusCode; +import 'json_test_common.dart' show Category, Colors, Platform, StatusCode; import 'json_test_example.dart'; Matcher _throwsArgumentError(matcher) => @@ -91,6 +91,21 @@ void main() { roundTripOrder(order); }); + test('case insensitive map', () { + final jsonOrder = {'category': 'CHaRmED', 'color': 'bLuE'}; + final order = Order.fromJson(jsonOrder); + expect(order.category, Category.charmed); + expect(order.color, Colors.blue); + }); + + test('case sensitive map throw', () { + expect( + () => Order.fromJson({'direction': 'dOwN'}), + _throwsArgumentError( + '`dOwN` is not one of the supported values: up, down, left, right'), + ); + }); + test('required, but missing enum value fails', () { expect( () => Person.fromJson({ diff --git a/json_serializable/test/integration/json_test_common.dart b/json_serializable/test/integration/json_test_common.dart index fb7c4d212..bdb0f5ad7 100644 --- a/json_serializable/test/integration/json_test_common.dart +++ b/json_serializable/test/integration/json_test_common.dart @@ -6,7 +6,7 @@ import 'dart:collection'; import 'package:json_annotation/json_annotation.dart'; -@JsonEnum(fieldRename: FieldRename.kebab) +@JsonEnum(fieldRename: FieldRename.kebab, caseInsensitive: true) enum Category { top, bottom, @@ -19,6 +19,10 @@ enum Category { notDiscoveredYet } +enum Colors { red, green, yellow, blue } + +enum Direction { up, down, left, right } + enum StatusCode { @JsonValue(200) success, diff --git a/json_serializable/test/integration/json_test_example.dart b/json_serializable/test/integration/json_test_example.dart index d69be6022..f60bf65d0 100644 --- a/json_serializable/test/integration/json_test_example.dart +++ b/json_serializable/test/integration/json_test_example.dart @@ -55,6 +55,9 @@ class Order { Duration? duration; final Category? category; + @JsonKey(caseInsensitive: true) + Colors? color; + Direction? direction; final UnmodifiableListView? items; Platform? platform; Map? altPlatforms; diff --git a/json_serializable/test/integration/json_test_example.g.dart b/json_serializable/test/integration/json_test_example.g.dart index 4681280b8..9407ac5d9 100644 --- a/json_serializable/test/integration/json_test_example.g.dart +++ b/json_serializable/test/integration/json_test_example.g.dart @@ -62,7 +62,8 @@ Order _$OrderFromJson(Map json) { disallowNullValues: const ['count'], ); return Order.custom( - $enumDecodeNullable(_$CategoryEnumMap, json['category']), + $enumDecodeNullable(_$CategoryEnumMap, json['category'], + comparator: $enumCompareCaseInsensitive), (json['items'] as List?) ?.map((e) => Item.fromJson(e as Map)), ) @@ -71,6 +72,9 @@ Order _$OrderFromJson(Map json) { ..duration = json['duration'] == null ? null : Duration(microseconds: json['duration'] as int) + ..color = $enumDecodeNullable(_$ColorsEnumMap, json['color'], + comparator: $enumCompareCaseInsensitive) + ..direction = $enumDecodeNullable(_$DirectionEnumMap, json['direction']) ..platform = json['platform'] == null ? null : Platform.fromJson(json['platform'] as String) @@ -80,7 +84,8 @@ Order _$OrderFromJson(Map json) { ..homepage = json['homepage'] == null ? null : Uri.parse(json['homepage'] as String) ..statusCode = $enumDecodeNullable(_$StatusCodeEnumMap, json['status_code'], - unknownValue: StatusCode.unknown) ?? + unknownValue: StatusCode.unknown, + comparator: $enumCompareCaseInsensitive) ?? StatusCode.success; } @@ -97,6 +102,8 @@ Map _$OrderToJson(Order instance) { val['isRushed'] = instance.isRushed; val['duration'] = instance.duration?.inMicroseconds; val['category'] = _$CategoryEnumMap[instance.category]; + val['color'] = _$ColorsEnumMap[instance.color]; + val['direction'] = _$DirectionEnumMap[instance.direction]; val['items'] = instance.items; val['platform'] = instance.platform; val['altPlatforms'] = instance.altPlatforms; @@ -105,6 +112,20 @@ Map _$OrderToJson(Order instance) { return val; } +const _$ColorsEnumMap = { + Colors.red: 'red', + Colors.green: 'green', + Colors.yellow: 'yellow', + Colors.blue: 'blue', +}; + +const _$DirectionEnumMap = { + Direction.up: 'up', + Direction.down: 'down', + Direction.left: 'left', + Direction.right: 'right', +}; + const _$StatusCodeEnumMap = { StatusCode.success: 200, StatusCode.notFound: 404,