Skip to content
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

Custom Map Types #1221

Open
wants to merge 3 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions json_serializable/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 6.6.0
- Allow custom map types to automatically have key types serialized.
Fixes[#396](https://github.com/google/json_serializable.dart/issues/396)

## 6.5.3

- Fixed handling of nullable `enum` fields with `includeIfNull: false`.
Expand Down
129 changes: 113 additions & 16 deletions json_serializable/lib/src/type_helpers/json_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ import 'package:source_helper/source_helper.dart';

import '../default_container.dart';
import '../type_helper.dart';
import '../unsupported_type_error.dart';
import '../utils.dart';
import 'config_types.dart';
import 'generic_factory_helper.dart';
import 'map_helper.dart';

const _helperLambdaParam = 'value';

Expand Down Expand Up @@ -49,11 +51,12 @@ class JsonHelper extends TypeHelper<TypeHelperContextWithConfig> {

toJsonArgs.addAll(
_helperParams(
context.serialize,
context,
_encodeHelper,
interfaceType,
toJson.parameters.where((element) => element.isRequiredPositional),
toJson,
isSerializing: true,
),
);
}
Expand Down Expand Up @@ -109,11 +112,12 @@ class JsonHelper extends TypeHelper<TypeHelperContextWithConfig> {
final args = [
output,
..._helperParams(
context.deserialize,
context,
_decodeHelper,
targetType,
positionalParams.skip(1),
fromJsonCtor,
isSerializing: false,
),
];

Expand All @@ -137,13 +141,15 @@ class JsonHelper extends TypeHelper<TypeHelperContextWithConfig> {
}

List<String> _helperParams(
Object? Function(DartType, String) execute,
TypeParameterType Function(ParameterElement, Element) paramMapper,
TypeHelperContextWithConfig context,
TypeParameterTypeWithKeyHelper Function(ParameterElement, Element)
paramMapper,
InterfaceType type,
Iterable<ParameterElement> positionalParams,
Element targetElement,
) {
final rest = <TypeParameterType>[];
Element targetElement, {
required bool isSerializing,
}) {
final rest = <TypeParameterTypeWithKeyHelper>[];
for (var param in positionalParams) {
rest.add(paramMapper(param, targetElement));
}
Expand All @@ -152,18 +158,28 @@ List<String> _helperParams(

for (var helperArg in rest) {
final typeParamIndex =
type.element2.typeParameters.indexOf(helperArg.element2);
type.element2.typeParameters.indexOf(helperArg.type.element2);

// TODO: throw here if `typeParamIndex` is -1 ?
final typeArg = type.typeArguments[typeParamIndex];
final body = execute(typeArg, _helperLambdaParam);
args.add('($_helperLambdaParam) => $body');
final body = isSerializing
? context.serialize(typeArg, _helperLambdaParam)
: context.deserialize(typeArg, _helperLambdaParam);
if (helperArg.isJsonKey) {
const keyHelper = MapKeyHelper();
final newBody = isSerializing
? keyHelper.serialize(typeArg, '', context)
: keyHelper.deserialize(typeArg, '', context, false);
args.add('($_helperLambdaParam) => $newBody');
} else {
args.add('($_helperLambdaParam) => $body');
}
}

return args;
}

TypeParameterType _decodeHelper(
TypeParameterTypeWithKeyHelper _decodeHelper(
ParameterElement param,
Element targetElement,
) {
Expand All @@ -178,8 +194,11 @@ TypeParameterType _decodeHelper(
final funcParamType = type.normalParameterTypes.single;

if ((funcParamType.isDartCoreObject && funcParamType.isNullableType) ||
funcParamType.isDynamic) {
return funcReturnType as TypeParameterType;
funcParamType.isDynamic ||
funcParamType.isDartCoreString) {
return TypeParameterTypeWithKeyHelper(
funcReturnType as TypeParameterType,
funcParamType.isDartCoreString);
}
}
}
Expand All @@ -194,20 +213,30 @@ TypeParameterType _decodeHelper(
);
}

TypeParameterType _encodeHelper(
class TypeParameterTypeWithKeyHelper {
final TypeParameterType type;
final bool isJsonKey;

TypeParameterTypeWithKeyHelper(this.type, this.isJsonKey);
}

TypeParameterTypeWithKeyHelper _encodeHelper(
ParameterElement param,
Element targetElement,
) {
final type = param.type;

if (type is FunctionType &&
(type.returnType.isDartCoreObject || type.returnType.isDynamic) &&
(type.returnType.isDartCoreObject ||
type.returnType.isDynamic ||
type.returnType.isDartCoreString) &&
type.normalParameterTypes.length == 1) {
final funcParamType = type.normalParameterTypes.single;

if (param.name == toJsonForName(funcParamType.element2!.name!)) {
if (funcParamType is TypeParameterType) {
return funcParamType;
return TypeParameterTypeWithKeyHelper(
funcParamType, type.returnType.isDartCoreString);
}
}
}
Expand Down Expand Up @@ -290,3 +319,71 @@ ClassConfig? _annotation(ClassConfig config, InterfaceType source) {
MethodElement? _toJsonMethod(DartType type) => type.typeImplementations
.map((dt) => dt is InterfaceType ? dt.getMethod('toJson') : null)
.firstWhereOrNull((me) => me != null);

class MapKeyHelper extends TypeHelper<TypeHelperContextWithConfig> {
const MapKeyHelper();

@override
String? serialize(
DartType targetType,
String expression,
TypeHelperContextWithConfig context,
) {
final keyType = targetType;

checkSafeMapKeyType(expression, keyType);

final subKeyValue = mapKeyHelperForType(keyType)
?.serialize(keyType, _helperLambdaParam, false) ??
context.serialize(keyType, _helperLambdaParam);

if (_helperLambdaParam == subKeyValue) {
return expression;
}

return '$subKeyValue';
}

@override
String? deserialize(
DartType targetType,
String expression,
TypeHelperContextWithConfig context,
bool defaultProvided,
) {
final keyArg = targetType;

checkSafeMapKeyType(expression, keyArg);

final isKeyStringable = isMapKeyStringable(keyArg);
if (!isKeyStringable) {
throw UnsupportedTypeError(
keyArg,
expression,
'Map keys must be one of: ${allowedMapKeyTypes.join(', ')}.',
);
}

String keyUsage;
if (keyArg.isEnum) {
keyUsage = context.deserialize(keyArg, _helperLambdaParam).toString();
} else if (context.config.anyMap &&
!(keyArg.isDartCoreObject || keyArg.isDynamic)) {
keyUsage = '$_helperLambdaParam as String';
} else if (context.config.anyMap &&
keyArg.isDartCoreObject &&
!keyArg.isNullableType) {
keyUsage = '$_helperLambdaParam as Object';
} else {
keyUsage = '$_helperLambdaParam as String';
}

final toFromString = mapKeyHelperForType(keyArg);
if (toFromString != null) {
keyUsage =
toFromString.deserialize(keyArg, keyUsage, false, true)!.toString();
}

return keyUsage;
}
}
20 changes: 10 additions & 10 deletions json_serializable/lib/src/type_helpers/map_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ class MapHelper extends TypeHelper<TypeHelperContextWithConfig> {
final keyType = args[0];
final valueType = args[1];

_checkSafeKeyType(expression, keyType);
checkSafeMapKeyType(expression, keyType);

final subFieldValue = context.serialize(valueType, closureArg);
final subKeyValue =
_forType(keyType)?.serialize(keyType, _keyParam, false) ??
mapKeyHelperForType(keyType)?.serialize(keyType, _keyParam, false) ??
context.serialize(keyType, _keyParam);

if (closureArg == subFieldValue && _keyParam == subKeyValue) {
Expand Down Expand Up @@ -66,11 +66,11 @@ class MapHelper extends TypeHelper<TypeHelperContextWithConfig> {
final keyArg = typeArgs.first;
final valueArg = typeArgs.last;

_checkSafeKeyType(expression, keyArg);
checkSafeMapKeyType(expression, keyArg);

final valueArgIsAny = valueArg.isDynamic ||
(valueArg.isDartCoreObject && valueArg.isNullableType);
final isKeyStringable = _isKeyStringable(keyArg);
final isKeyStringable = isMapKeyStringable(keyArg);

final targetTypeIsNullable = defaultProvided || targetType.isNullableType;
final optionalQuestion = targetTypeIsNullable ? '?' : '';
Expand Down Expand Up @@ -124,7 +124,7 @@ class MapHelper extends TypeHelper<TypeHelperContextWithConfig> {
keyUsage = _keyParam;
}

final toFromString = _forType(keyArg);
final toFromString = mapKeyHelperForType(keyArg);
if (toFromString != null) {
keyUsage =
toFromString.deserialize(keyArg, keyUsage, false, true).toString();
Expand All @@ -146,22 +146,22 @@ final _instances = [
uriString,
];

ToFromStringHelper? _forType(DartType type) =>
ToFromStringHelper? mapKeyHelperForType(DartType type) =>
_instances.singleWhereOrNull((i) => i.matches(type));

/// Returns `true` if [keyType] can be automatically converted to/from String –
/// and is therefor usable as a key in a [Map].
bool _isKeyStringable(DartType keyType) =>
bool isMapKeyStringable(DartType keyType) =>
keyType.isEnum || _instances.any((inst) => inst.matches(keyType));

void _checkSafeKeyType(String expression, DartType keyArg) {
void checkSafeMapKeyType(String expression, DartType keyArg) {
// We're not going to handle converting key types at the moment
// So the only safe types for key are dynamic/Object/String/enum
if (keyArg.isDynamic ||
(!keyArg.isNullableType &&
(keyArg.isDartCoreObject ||
coreStringTypeChecker.isExactlyType(keyArg) ||
_isKeyStringable(keyArg)))) {
isMapKeyStringable(keyArg)))) {
return;
}

Expand All @@ -174,7 +174,7 @@ void _checkSafeKeyType(String expression, DartType keyArg) {

/// The names of types that can be used as [Map] keys.
///
/// Used in [_checkSafeKeyType] to provide a helpful error with unsupported
/// Used in [checkSafeMapKeyType] to provide a helpful error with unsupported
/// types.
List<String> get allowedMapKeyTypes => [
'Object',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,33 @@ class ConcreteClass {

Map<String, dynamic> toJson() => _$ConcreteClassToJson(this);
}

class CustomMap<K, V> {
final Map<K, V> map;

CustomMap(this.map);

factory CustomMap.fromJson(
Map<String, dynamic> json,
K Function(String?) fromJsonK,
V Function(Object?) fromJsonV,
) =>
CustomMap(json.map<K, V>(
(key, value) => MapEntry(fromJsonK(key), fromJsonV(value))));

Map<String?, dynamic> toJson(
String? Function(K) toJsonK, Object? Function(V) toJsonV) =>
map.map((key, value) => MapEntry(toJsonK(key), toJsonV(value)));
}

@JsonSerializable()
class UseOfCustomMap {
final CustomMap<int, String> map;

UseOfCustomMap(this.map);

factory UseOfCustomMap.fromJson(Map<String, dynamic> json) =>
_$UseOfCustomMapFromJson(json);

Map<String, dynamic> toJson() => _$UseOfCustomMapToJson(this);
}

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