Skip to content
Open
24 changes: 22 additions & 2 deletions json_annotation/lib/src/enum_helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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>(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>(V arg1, Object arg2) => arg1 == arg2;

/// Returns the key associated with value [source] from [enumValues], if one
/// exists.
///
Expand All @@ -18,13 +33,16 @@ K? $enumDecodeNullable<K extends Enum, V>(
Map<K, V> 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;
}
}
Expand Down Expand Up @@ -65,6 +83,7 @@ K $enumDecode<K extends Enum, V>(
Map<K, V> enumValues,
Object? source, {
K? unknownValue,
bool Function(V arg1, Object arg2)? comparator,
}) {
if (source == null) {
throw ArgumentError(
Expand All @@ -73,8 +92,9 @@ K $enumDecode<K extends Enum, V>(
);
}

comparator ??= $enumCompareStandard;
for (var entry in enumValues.entries) {
if (entry.value == source) {
if (comparator(entry.value, source)) {
return entry.key;
}
}
Expand Down
7 changes: 7 additions & 0 deletions json_annotation/lib/src/json_enum.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class JsonEnum {
const JsonEnum({
this.alwaysCreate = false,
this.fieldRename = FieldRename.none,
this.caseInsensitive = false,
this.valueField,
});

Expand All @@ -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.
///
Expand Down
12 changes: 10 additions & 2 deletions json_annotation/lib/src/json_key.dart
Original file line number Diff line number Diff line change
Expand Up @@ -149,19 +149,26 @@ 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,
@Deprecated(
'Use `includeFromJson` and `includeToJson` with a value of `false` '
'instead.',
)
this.ignore,
this.ignore,
this.includeFromJson,
this.includeIfNull,
this.includeToJson,
Expand All @@ -170,6 +177,7 @@ class JsonKey {
this.required,
this.toJson,
this.unknownEnumValue,
this.caseInsensitive = false,
});

/// Sentinel value for use with [unknownEnumValue].
Expand Down
6 changes: 5 additions & 1 deletion json_serializable/lib/src/enum_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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?,
);
}
Expand Down
8 changes: 8 additions & 0 deletions json_serializable/lib/src/json_key_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
Expand Down Expand Up @@ -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?,
);
}

Expand All @@ -286,6 +292,7 @@ KeyConfig _populateJsonKey(
String? readValueFunctionName,
bool? required,
String? unknownEnumValue,
bool? caseInsensitive,
bool? includeToJson,
bool? includeFromJson,
}) {
Expand All @@ -307,6 +314,7 @@ KeyConfig _populateJsonKey(
readValueFunctionName: readValueFunctionName,
required: required ?? false,
unknownEnumValue: unknownEnumValue,
caseInsensitive: caseInsensitive,
includeFromJson: includeFromJson,
includeToJson: includeToJson,
);
Expand Down
3 changes: 3 additions & 0 deletions json_serializable/lib/src/type_helpers/config_types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ class KeyConfig {

final String? unknownEnumValue;

final bool? caseInsensitive;

final String? readValueFunctionName;

KeyConfig({
Expand All @@ -35,6 +37,7 @@ class KeyConfig {
required this.readValueFunctionName,
required this.required,
required this.unknownEnumValue,
required this.caseInsensitive,
});
}

Expand Down
2 changes: 2 additions & 0 deletions json_serializable/lib/src/type_helpers/enum_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ class EnumHelper extends TypeHelper<TypeHelperContextWithConfig> {
expression,
if (jsonKey.unknownEnumValue != null)
'unknownValue: ${jsonKey.unknownEnumValue}',
if ((jsonKey.caseInsensitive ?? false) == true)
r'comparator: $enumCompareCaseInsensitive',
];

return '$functionName(${args.join(', ')})';
Expand Down
10 changes: 10 additions & 0 deletions json_serializable/lib/src/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand Down
17 changes: 16 additions & 1 deletion json_serializable/test/integration/integration_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down Expand Up @@ -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({
Expand Down
6 changes: 5 additions & 1 deletion json_serializable/test/integration/json_test_common.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -19,6 +19,10 @@ enum Category {
notDiscoveredYet
}

enum Colors { red, green, yellow, blue }

enum Direction { up, down, left, right }

enum StatusCode {
@JsonValue(200)
success,
Expand Down
3 changes: 3 additions & 0 deletions json_serializable/test/integration/json_test_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ class Order {
Duration? duration;

final Category? category;
@JsonKey(caseInsensitive: true)
Colors? color;
Direction? direction;
final UnmodifiableListView<Item>? items;
Platform? platform;
Map<String, Platform>? altPlatforms;
Expand Down
25 changes: 23 additions & 2 deletions json_serializable/test/integration/json_test_example.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.