Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: google/json_serializable.dart
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: master
Choose a base ref
...
head repository: TimWhiting/json_serializable.dart
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: custom_map_types
Choose a head ref
  • 3 commits
  • 5 files changed
  • 1 contributor

Commits on Oct 8, 2022

  1. Custom Map Types

    Custom Map Types
    TimWhiting committed Oct 8, 2022

    Verified

    This commit was signed with the committer’s verified signature.
    TimWhiting Tim Whiting
    Copy the full SHA
    718b6e5 View commit details

Commits on Oct 19, 2022

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    166f340 View commit details
  2. Copy the full SHA
    9dec114 View commit details
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`.
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
@@ -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';

@@ -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,
),
);
}
@@ -109,11 +112,12 @@ class JsonHelper extends TypeHelper<TypeHelperContextWithConfig> {
final args = [
output,
..._helperParams(
context.deserialize,
context,
_decodeHelper,
targetType,
positionalParams.skip(1),
fromJsonCtor,
isSerializing: false,
),
];

@@ -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));
}
@@ -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,
) {
@@ -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);
}
}
}
@@ -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);
}
}
}
@@ -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
@@ -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) {
@@ -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 ? '?' : '';
@@ -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();
@@ -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;
}

@@ -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',
Original file line number Diff line number Diff line change
@@ -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.