Skip to content

Add aliases to JsonValue to enum value to be decoded from different JSON values #1459

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
33 changes: 28 additions & 5 deletions _test_yaml/test/src/build_config.g.dart

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

85 changes: 85 additions & 0 deletions json_annotation/lib/src/enum_helpers.dart
Original file line number Diff line number Diff line change
@@ -51,6 +51,53 @@ K? $enumDecodeNullable<K extends Enum, V>(
return unknownValue;
}

/// Returns the key associated with value [source] from [decodeMap], if one
/// exists.
///
/// If [unknownValue] is not `null` and [source] is not a value in [decodeMap],
/// [unknownValue] is returned. Otherwise, an [ArgumentError] is thrown.
///
/// If [source] is `null`, `null` is returned.
///
/// Exposed only for code generated by `package:json_serializable`.
/// Not meant to be used directly by user code.
V? $enumDecodeNullableWithDecodeMap<K, V extends Enum>(
Map<K, V> decodeMap,
Object? source, {
Enum? unknownValue,
}) {
if (source == null) {
return null;
}

final decodedValue = decodeMap[source];

if (decodedValue != null) {
return decodedValue;
}

if (unknownValue == JsonKey.nullForUndefinedEnumValue) {
return null;
}

if (unknownValue == null) {
throw ArgumentError(
'`$source` is not one of the supported values: '
'${decodeMap.keys.join(', ')}',
);
}

if (unknownValue is! V) {
throw ArgumentError.value(
unknownValue,
'unknownValue',
'Must by of type `$K` or `JsonKey.nullForUndefinedEnumValue`.',
);
}

return unknownValue;
}

/// Returns the key associated with value [source] from [enumValues], if one
/// exists.
///
@@ -88,3 +135,41 @@ K $enumDecode<K extends Enum, V>(

return unknownValue;
}

/// Returns the key associated with value [source] from [decodeMap], if one
/// exists.
///
/// If [unknownValue] is not `null` and [source] is not a value in [decodeMap],
/// [unknownValue] is returned. Otherwise, an [ArgumentError] is thrown.
///
/// If [source] is `null`, an [ArgumentError] is thrown.
///
/// Exposed only for code generated by `package:json_serializable`.
/// Not meant to be used directly by user code.
V $enumDecodeWithDecodeMap<K, V extends Enum>(
Map<K, V> decodeMap,
Object? source, {
V? unknownValue,
}) {
if (source == null) {
throw ArgumentError(
'A value must be provided. Supported values: '
'${decodeMap.keys.join(', ')}',
);
}

final decodedValue = decodeMap[source];

if (decodedValue != null) {
return decodedValue;
}

if (unknownValue == null) {
throw ArgumentError(
'`$source` is not one of the supported values: '
'${decodeMap.keys.join(', ')}',
);
}

return unknownValue;
}
7 changes: 6 additions & 1 deletion json_annotation/lib/src/json_value.dart
Original file line number Diff line number Diff line change
@@ -9,5 +9,10 @@ class JsonValue {
/// Can be a [String] or an [int].
final dynamic value;

const JsonValue(this.value);
/// Optional values that can be used when deserializing.
///
/// The elements of [aliases] must be either [String] or [int].
final Set<Object> aliases;

const JsonValue(this.value, {this.aliases = const {}});
}
109 changes: 100 additions & 9 deletions json_serializable/lib/src/enum_utils.dart
Original file line number Diff line number Diff line change
@@ -15,6 +15,9 @@ import 'utils.dart';
String constMapName(DartType targetType) =>
'_\$${targetType.element!.name}EnumMap';

String constDecodeMapName(DartType targetType) =>
'_\$${targetType.element!.name}EnumDecodeMap';

/// If [targetType] is not an enum, return `null`.
///
/// Otherwise, returns `true` if [targetType] is nullable OR if one of the
@@ -31,21 +34,45 @@ bool? enumFieldWithNullInEncodeMap(DartType targetType) {
return enumMap.values.contains(null);
}

String? enumValueMapFromType(
String? enumMapsFromType(
DartType targetType, {
bool nullWithNoAnnotation = false,
}) {
final enumMap =
_enumMap(targetType, nullWithNoAnnotation: nullWithNoAnnotation);

if (enumMap == null) return null;

final items = enumMap.entries
.map((e) => ' ${targetType.element!.name}.${e.key.name}: '
'${jsonLiteralAsDart(e.value)},')
.join();

return 'const ${constMapName(targetType)} = {\n$items\n};';
final enumAliases =
_enumAliases(targetType, nullWithNoAnnotation: nullWithNoAnnotation);

final valuesItems = enumMap == null
? null
: [
for (final MapEntry(:key, :value) in enumMap.entries)
' ${targetType.element!.name}.${key.name}: '
'${jsonLiteralAsDart(value)},',
].join();

final valuesMap = valuesItems == null
? null
: '// ignore: unused_element\n'
'const ${constMapName(targetType)} = {\n$valuesItems\n};';

final decodeItems = enumAliases == null
? null
: [
for (final MapEntry(:key, :value) in enumAliases.entries)
' ${jsonLiteralAsDart(key)}: '
'${targetType.element!.name}.${value.name},',
].join();

final decodeMap = decodeItems == null
? null
: '// ignore: unused_element\n'
'const ${constDecodeMapName(targetType)} = {\n$decodeItems\n};';

return valuesMap == null && decodeMap == null
? null
: [valuesMap, decodeMap].join('\n\n');
}

Map<FieldElement, Object?>? _enumMap(
@@ -73,6 +100,34 @@ Map<FieldElement, Object?>? _enumMap(
};
}

Map<Object?, FieldElement>? _enumAliases(
DartType targetType, {
bool nullWithNoAnnotation = false,
}) {
final targetTypeElement = targetType.element;
if (targetTypeElement == null) return null;
final annotation = _jsonEnumChecker.firstAnnotationOf(targetTypeElement);
final jsonEnum = _fromAnnotation(annotation);

final enumFields = iterateEnumFields(targetType);

if (enumFields == null || (nullWithNoAnnotation && !jsonEnum.alwaysCreate)) {
return null;
}

return {
for (var field in enumFields) ...{
_generateEntry(
field: field,
jsonEnum: jsonEnum,
targetType: targetType,
): field,
for (var alias in _generateAliases(field: field, targetType: targetType))
alias: field,
},
};
}

Object? _generateEntry({
required FieldElement field,
required JsonEnum jsonEnum,
@@ -138,6 +193,36 @@ Object? _generateEntry({
}
}

List<Object?> _generateAliases({
required FieldElement field,
required DartType targetType,
}) {
final annotation =
const TypeChecker.fromRuntime(JsonValue).firstAnnotationOfExact(field);

if (annotation == null) {
return const [];
} else {
final reader = ConstantReader(annotation);

final valueReader = reader.read('aliases');

if (valueReader.validAliasesType) {
return [
for (final value in valueReader.setValue)
ConstantReader(value).literalValue,
];
} else {
final targetTypeCode = typeToCode(targetType);
throw InvalidGenerationSourceError(
'The `JsonValue` annotation on `$targetTypeCode.${field.name}` aliases '
'should all be of type String or int.',
element: field,
);
}
}
}

const _jsonEnumChecker = TypeChecker.fromRuntime(JsonEnum);

JsonEnum _fromAnnotation(DartObject? dartObject) {
@@ -154,4 +239,10 @@ JsonEnum _fromAnnotation(DartObject? dartObject) {

extension on ConstantReader {
bool get validValueType => isString || isNull || isInt;

bool get validAliasesType =>
isSet &&
setValue.every((element) =>
(element.type?.isDartCoreString ?? false) ||
(element.type?.isDartCoreInt ?? false));
}
2 changes: 1 addition & 1 deletion json_serializable/lib/src/json_enum_generator.dart
Original file line number Diff line number Diff line change
@@ -26,7 +26,7 @@ class JsonEnumGenerator extends GeneratorForAnnotation<JsonEnum> {
}

final value =
enumValueMapFromType(element.thisType, nullWithNoAnnotation: true);
enumMapsFromType(element.thisType, nullWithNoAnnotation: true);

return [
if (value != null) value,
10 changes: 5 additions & 5 deletions json_serializable/lib/src/type_helpers/enum_helper.dart
Original file line number Diff line number Diff line change
@@ -21,7 +21,7 @@ class EnumHelper extends TypeHelper<TypeHelperContextWithConfig> {
String expression,
TypeHelperContextWithConfig context,
) {
final memberContent = enumValueMapFromType(targetType);
final memberContent = enumMapsFromType(targetType);

if (memberContent == null) {
return null;
@@ -44,7 +44,7 @@ class EnumHelper extends TypeHelper<TypeHelperContextWithConfig> {
TypeHelperContextWithConfig context,
bool defaultProvided,
) {
final memberContent = enumValueMapFromType(targetType);
final memberContent = enumMapsFromType(targetType);

if (memberContent == null) {
return null;
@@ -64,15 +64,15 @@ class EnumHelper extends TypeHelper<TypeHelperContextWithConfig> {

String functionName;
if (targetType.isNullableType || defaultProvided) {
functionName = r'$enumDecodeNullable';
functionName = r'$enumDecodeNullableWithDecodeMap';
} else {
functionName = r'$enumDecode';
functionName = r'$enumDecodeWithDecodeMap';
}

context.addMember(memberContent);

final args = [
constMapName(targetType),
constDecodeMapName(targetType),
expression,
if (jsonKey.unknownEnumValue != null)
'unknownValue: ${jsonKey.unknownEnumValue}',
Loading