Skip to content

Commit de7cf91

Browse files
committed
Support more non-String Map keys of obvious dart:core types
Partially addresses #396
1 parent 2dceb1b commit de7cf91

15 files changed

+589
-32
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

+38-7
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

@@ -28,15 +29,17 @@ class MapHelper extends TypeHelper<TypeHelperContextWithConfig> {
2829

2930
_checkSafeKeyType(expression, keyType);
3031

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

3438
if (closureArg == subFieldValue && _keyParam == subKeyValue) {
3539
return expression;
3640
}
3741

3842
final optionalQuestion = context.nullable ? '?' : '';
39-
4043
return '$expression$optionalQuestion'
4144
'.map(($_keyParam, $closureArg) => MapEntry($subKeyValue, $subFieldValue))';
4245
}
@@ -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 = _isStringKeyable(keyArg);
6063

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

9295
String keyUsage;
93-
if (isEnumKey) {
96+
97+
if (isEnum(keyArg)) {
9498
keyUsage = context.deserialize(keyArg, _keyParam).toString();
9599
} else if (context.config.anyMap && !_isObjectOrDynamic(keyArg)) {
96100
keyUsage = '$_keyParam as String';
97101
} else {
98102
keyUsage = _keyParam;
99103
}
100104

105+
final toFromString = _forType(keyArg);
106+
if (toFromString != null) {
107+
keyUsage = toFromString.deserialize(keyArg, keyUsage, false, true);
108+
}
109+
101110
return '($expression $mapCast)$optionalQuestion.map('
102111
'($_keyParam, $closureArg) => MapEntry($keyUsage, $itemSubVal),)';
103112
}
104113
}
105114

115+
final _intString = ToFromStringHelper('int.parse', 'toString()', 'int');
116+
117+
final _instances = {
118+
bigIntString,
119+
dateTimeString,
120+
_intString,
121+
uriString,
122+
};
123+
124+
Iterable<String> get _allowedTypeNames => const [
125+
'Object',
126+
'dynamic',
127+
'enum',
128+
'String'
129+
].followedBy(_instances.map((i) => i.coreTypeName));
130+
131+
ToFromStringHelper _forType(DartType type) =>
132+
_instances.singleWhere((i) => i.matches(type), orElse: () => null);
133+
106134
bool _isObjectOrDynamic(DartType type) => type.isObject || type.isDynamic;
107135

136+
bool _isStringKeyable(DartType keyType) =>
137+
isEnum(keyType) || _instances.any((inst) => inst.matches(keyType));
138+
108139
void _checkSafeKeyType(String expression, DartType keyArg) {
109140
// We're not going to handle converting key types at the moment
110141
// So the only safe types for key are dynamic/Object/String/enum
111142
final safeKey = _isObjectOrDynamic(keyArg) ||
112143
coreStringTypeChecker.isExactlyType(keyArg) ||
113-
isEnum(keyArg);
144+
_isStringKeyable(keyArg);
114145

115146
if (!safeKey) {
116147
throw UnsupportedTypeError(keyArg, expression,
117-
'Map keys must be of type `String`, enum, `Object` or `dynamic`.');
148+
'Map keys must be one of: ${_allowedTypeNames.join(', ')}.');
118149
}
119150
}

json_serializable/lib/src/type_helpers/to_from_string.dart

+10-18
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,12 @@ import 'package:source_gen/source_gen.dart';
77

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

10-
const bigIntString = ToFromStringHelper(
11-
'BigInt.parse',
12-
'toString()',
13-
TypeChecker.fromUrl('dart:core#BigInt'),
14-
);
15-
16-
const dateTimeString = ToFromStringHelper(
17-
'DateTime.parse',
18-
'toIso8601String()',
19-
TypeChecker.fromUrl('dart:core#DateTime'),
20-
);
21-
22-
const uriString = ToFromStringHelper(
23-
'Uri.parse',
24-
'toString()',
25-
TypeChecker.fromUrl('dart:core#Uri'),
26-
);
10+
final bigIntString = ToFromStringHelper('BigInt.parse', 'toString()', 'BigInt');
11+
12+
final dateTimeString =
13+
ToFromStringHelper('DateTime.parse', 'toIso8601String()', 'DateTime');
14+
15+
final uriString = ToFromStringHelper('Uri.parse', 'toString()', 'Uri');
2716

2817
/// Package-internal helper that unifies implementations of [Type]s that convert
2918
/// trivially to-from [String].
@@ -40,9 +29,12 @@ class ToFromStringHelper {
4029
///
4130
/// Examples: `toString()` for a function or `stringValue` for a property.
4231
final String _toString;
32+
33+
final String coreTypeName;
4334
final TypeChecker _checker;
4435

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

4739
bool matches(DartType type) => _checker.isExactlyType(type);
4840

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 <[email protected]>
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)