Skip to content

Commit a192130

Browse files
authored
Support more non-String Map keys of obvious dart:core types (#493)
Partially addresses #396
1 parent 4d33182 commit a192130

15 files changed

+594
-20
lines changed

json_serializable/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 3.1.0
2+
3+
- Support `Map` keys of type `int`, `BigInt`, `DateTime`, and `Uri`.
4+
15
## 3.0.0
26

37
This release is entirely **BREAKING** changes. It removes underused features

json_serializable/lib/src/type_helpers/map_helper.dart

+44-6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import '../constants.dart';
88
import '../shared_checkers.dart';
99
import '../type_helper.dart';
1010
import '../utils.dart';
11+
import 'to_from_string.dart';
1112

1213
const _keyParam = 'k';
1314

@@ -29,7 +30,9 @@ class MapHelper extends TypeHelper<TypeHelperContextWithConfig> {
2930
_checkSafeKeyType(expression, keyType);
3031

3132
final subFieldValue = context.serialize(valueType, closureArg);
32-
final subKeyValue = context.serialize(keyType, _keyParam);
33+
final subKeyValue =
34+
_forType(keyType)?.serialize(keyType, _keyParam, false) ??
35+
context.serialize(keyType, _keyParam);
3336

3437
if (closureArg == subFieldValue && _keyParam == subKeyValue) {
3538
return expression;
@@ -56,9 +59,9 @@ class MapHelper extends TypeHelper<TypeHelperContextWithConfig> {
5659
_checkSafeKeyType(expression, keyArg);
5760

5861
final valueArgIsAny = _isObjectOrDynamic(valueArg);
59-
final isEnumKey = isEnum(keyArg);
62+
final isKeyStringable = _isKeyStringable(keyArg);
6063

61-
if (!isEnumKey) {
64+
if (!isKeyStringable) {
6265
if (valueArgIsAny) {
6366
if (context.config.anyMap) {
6467
if (_isObjectOrDynamic(keyArg)) {
@@ -90,30 +93,65 @@ class MapHelper extends TypeHelper<TypeHelperContextWithConfig> {
9093
context.config.anyMap ? 'as Map' : 'as Map<String, dynamic>';
9194

9295
String keyUsage;
93-
if (isEnumKey) {
96+
if (isEnum(keyArg)) {
9497
keyUsage = context.deserialize(keyArg, _keyParam).toString();
9598
} else if (context.config.anyMap && !_isObjectOrDynamic(keyArg)) {
9699
keyUsage = '$_keyParam as String';
97100
} else {
98101
keyUsage = _keyParam;
99102
}
100103

104+
final toFromString = _forType(keyArg);
105+
if (toFromString != null) {
106+
keyUsage = toFromString.deserialize(keyArg, keyUsage, false, true);
107+
}
108+
101109
return '($expression $mapCast)$optionalQuestion.map('
102110
'($_keyParam, $closureArg) => MapEntry($keyUsage, $itemSubVal),)';
103111
}
104112
}
105113

114+
final _intString = ToFromStringHelper('int.parse', 'toString()', 'int');
115+
116+
/// [ToFromStringHelper] instances representing non-String types that can
117+
/// be used as [Map] keys.
118+
final _instances = [
119+
bigIntString,
120+
dateTimeString,
121+
_intString,
122+
uriString,
123+
];
124+
125+
ToFromStringHelper _forType(DartType type) =>
126+
_instances.singleWhere((i) => i.matches(type), orElse: () => null);
127+
106128
bool _isObjectOrDynamic(DartType type) => type.isObject || type.isDynamic;
107129

130+
/// Returns `true` if [keyType] can be automatically converted to/from String –
131+
/// and is therefor usable as a key in a [Map].
132+
bool _isKeyStringable(DartType keyType) =>
133+
isEnum(keyType) || _instances.any((inst) => inst.matches(keyType));
134+
108135
void _checkSafeKeyType(String expression, DartType keyArg) {
109136
// We're not going to handle converting key types at the moment
110137
// So the only safe types for key are dynamic/Object/String/enum
111138
final safeKey = _isObjectOrDynamic(keyArg) ||
112139
coreStringTypeChecker.isExactlyType(keyArg) ||
113-
isEnum(keyArg);
140+
_isKeyStringable(keyArg);
114141

115142
if (!safeKey) {
116143
throw UnsupportedTypeError(keyArg, expression,
117-
'Map keys must be of type `String`, enum, `Object` or `dynamic`.');
144+
'Map keys must be one of: ${_allowedTypeNames.join(', ')}.');
118145
}
119146
}
147+
148+
/// The names of types that can be used as [Map] keys.
149+
///
150+
/// Used in [_checkSafeKeyType] to provide a helpful error with unsupported
151+
/// types.
152+
Iterable<String> get _allowedTypeNames => const [
153+
'Object',
154+
'dynamic',
155+
'enum',
156+
'String',
157+
].followedBy(_instances.map((i) => i.coreTypeName));

json_serializable/lib/src/type_helpers/to_from_string.dart

+9-7
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,22 @@ import 'package:source_gen/source_gen.dart';
77

88
import '../type_helper.dart';
99

10-
const bigIntString = ToFromStringHelper(
10+
final bigIntString = ToFromStringHelper(
1111
'BigInt.parse',
1212
'toString()',
13-
TypeChecker.fromUrl('dart:core#BigInt'),
13+
'BigInt',
1414
);
1515

16-
const dateTimeString = ToFromStringHelper(
16+
final dateTimeString = ToFromStringHelper(
1717
'DateTime.parse',
1818
'toIso8601String()',
19-
TypeChecker.fromUrl('dart:core#DateTime'),
19+
'DateTime',
2020
);
2121

22-
const uriString = ToFromStringHelper(
22+
final uriString = ToFromStringHelper(
2323
'Uri.parse',
2424
'toString()',
25-
TypeChecker.fromUrl('dart:core#Uri'),
25+
'Uri',
2626
);
2727

2828
/// Package-internal helper that unifies implementations of [Type]s that convert
@@ -40,9 +40,11 @@ class ToFromStringHelper {
4040
///
4141
/// Examples: `toString()` for a function or `stringValue` for a property.
4242
final String _toString;
43+
final String coreTypeName;
4344
final TypeChecker _checker;
4445

45-
const ToFromStringHelper(this._parse, this._toString, this._checker);
46+
ToFromStringHelper(this._parse, this._toString, this.coreTypeName)
47+
: _checker = TypeChecker.fromUrl('dart:core#$coreTypeName');
4648

4749
bool matches(DartType type) => _checker.isExactlyType(type);
4850

json_serializable/pubspec.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: json_serializable
2-
version: 3.0.0
2+
version: 3.1.0-dev
33
author: Dart Team <misc@dartlang.org>
44
description: >-
55
Automatically generate code for converting to and from JSON by annotating

json_serializable/test/integration/integration_test.dart

+13
Original file line numberDiff line numberDiff line change
@@ -232,4 +232,17 @@ void main() {
232232
expect(() => Numbers.fromJson(value), throwsCastError);
233233
});
234234
});
235+
236+
test('MapKeyVariety', () {
237+
final instance = MapKeyVariety()
238+
..bigIntMap = {BigInt.from(1): 1}
239+
..dateTimeIntMap = {DateTime.parse('2018-01-01'): 2}
240+
..intIntMap = {3: 3}
241+
..uriIntMap = {Uri.parse('https://example.com'): 4};
242+
243+
final roundTrip =
244+
roundTripObject(instance, (j) => MapKeyVariety.fromJson(j));
245+
246+
expect(roundTrip, instance);
247+
});
235248
}

json_serializable/test/integration/json_test_example.dart

+24
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import 'dart:collection';
77

88
import 'package:json_annotation/json_annotation.dart';
9+
910
import 'json_test_common.dart';
1011

1112
part 'json_test_example.g.dart';
@@ -146,3 +147,26 @@ class Numbers {
146147
deepEquals(duration, other.duration) &&
147148
deepEquals(date, other.date);
148149
}
150+
151+
@JsonSerializable()
152+
class MapKeyVariety {
153+
Map<int, int> intIntMap;
154+
Map<Uri, int> uriIntMap;
155+
Map<DateTime, int> dateTimeIntMap;
156+
Map<BigInt, int> bigIntMap;
157+
158+
MapKeyVariety();
159+
160+
factory MapKeyVariety.fromJson(Map<String, dynamic> json) =>
161+
_$MapKeyVarietyFromJson(json);
162+
163+
Map<String, dynamic> toJson() => _$MapKeyVarietyToJson(this);
164+
165+
@override
166+
bool operator ==(Object other) =>
167+
other is MapKeyVariety &&
168+
deepEquals(other.intIntMap, intIntMap) &&
169+
deepEquals(other.uriIntMap, uriIntMap) &&
170+
deepEquals(other.dateTimeIntMap, dateTimeIntMap) &&
171+
deepEquals(other.bigIntMap, bigIntMap);
172+
}

json_serializable/test/integration/json_test_example.g.dart

+25
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)