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 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Expand Up @@ -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.
///
Expand Down Expand Up @@ -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
Expand Up @@ -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 {}});
}
107 changes: 99 additions & 8 deletions json_serializable/lib/src/enum_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -38,14 +41,38 @@ String? enumValueMapFromType(
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.element3!.name3}.${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(
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand All @@ -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));
}
6 changes: 3 additions & 3 deletions json_serializable/lib/src/type_helpers/enum_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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}',
Expand Down
13 changes: 12 additions & 1 deletion json_serializable/test/default_value/default_value.g.dart

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

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

Loading