Skip to content

Commit b71fb75

Browse files
authored
Support generating a custom map and using non-copying wrappers for serialization (#48)
1 parent ae3eb0f commit b71fb75

21 files changed

+1678
-163
lines changed

json_annotation/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 0.2.2
2+
3+
* Added a helper class – `$JsonMapWrapper` – and helper functions – `$wrapMap`,
4+
`$wrapMapHandleNull`, `$wrapList`, and `$wrapListHandleNull` – to support
5+
the `useWrappers` option added to `JsonSerializableGenerator` in `v0.3.0` of
6+
`package:json_serializable`.
7+
18
## 0.2.1
29

310
* `JsonSerializable` class annotation

json_annotation/lib/src/json_serializable.dart

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
import 'dart:collection';
6+
57
class JsonSerializable {
68
// TODO(kevmoo): document these fields
79
final bool createFactory;
@@ -68,3 +70,55 @@ class JsonKey {
6870
/// Only required when the default behavior is not desired.
6971
const JsonKey({this.name, this.nullable, this.includeIfNull});
7072
}
73+
74+
// TODO(kevmoo): Add documentation
75+
abstract class $JsonMapWrapper extends UnmodifiableMapBase<String, dynamic> {}
76+
77+
Map<String, dynamic> $wrapMap<K, V>(
78+
Map<K, V> source, dynamic converter(V key)) =>
79+
new _MappingMap(source, converter);
80+
81+
Map<String, dynamic> $wrapMapHandleNull<K, V>(
82+
Map<K, V> source, dynamic converter(V key)) =>
83+
source == null ? null : new _MappingMap(source, converter);
84+
85+
List<dynamic> $wrapList<T>(List<T> source, dynamic converter(T key)) =>
86+
new _MappingList(source, converter);
87+
88+
List<dynamic> $wrapListHandleNull<T>(
89+
List<T> source, dynamic converter(T key)) =>
90+
source == null ? null : new _MappingList(source, converter);
91+
92+
typedef dynamic _Convert<S>(S value);
93+
94+
class _MappingList<S> extends ListBase<dynamic> {
95+
final List<S> _source;
96+
final _Convert<S> _converter;
97+
98+
_MappingList(this._source, this._converter);
99+
100+
@override
101+
dynamic operator [](int index) => _converter(_source[index]);
102+
103+
@override
104+
operator []=(int index, dynamic value) => throw new UnsupportedError('');
105+
106+
@override
107+
int get length => _source.length;
108+
109+
@override
110+
set length(int value) => throw new UnsupportedError('');
111+
}
112+
113+
class _MappingMap<K, V> extends UnmodifiableMapBase<String, dynamic> {
114+
final Map<K, V> _source;
115+
final _Convert<V> _converter;
116+
117+
_MappingMap(this._source, this._converter);
118+
119+
@override
120+
Iterable<String> get keys => _source.keys.map((k) => k as String);
121+
122+
@override
123+
dynamic operator [](Object key) => _converter(_source[key]);
124+
}

json_annotation/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: json_annotation
2-
version: 0.2.1
2+
version: 0.2.2-dev
33
description: Annotations for the json_serializable package
44
homepage: https://github.com/dart-lang/json_serializable
55
author: Dart Team <misc@dartlang.org>

json_serializable/CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@
66
* **BREAKING** The deprecated `annotations.dart` library has been removed.
77
Use `package:json_annotation` instead.
88

9+
* **BREAKING** The arguments to `TypeHelper` `serialize` and `deserialize` have
10+
changed.
11+
* `SerializeContext` and `DeserializeContext` (new classes) are now passed
12+
instead of the `TypeHelperGenerator` typedef (which has been deleted).
13+
14+
* `JsonSerializableGenerator` now supports an optional `useWrappers` argument
15+
when generates and uses wrapper classes to (hopefully) improve the speed and
16+
memory usage of serialization – at the cost of more code.
17+
18+
**NOTE**: `useWrappers` is not guaranteed to improve the performance of
19+
serialization. Benchmarking is recommended.
20+
921
* Make `null` field handling smarter. If a field is classified as not
1022
`nullable`, then use this knowledge when generating serialization code – even
1123
if `includeIfNull` is `false`.

json_serializable/lib/src/json_serializable_generator.dart

Lines changed: 114 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,26 @@ class JsonSerializableGenerator
3636

3737
final List<TypeHelper> _typeHelpers;
3838

39+
final bool useWrappers;
40+
3941
/// Creates an instance of [JsonSerializableGenerator].
4042
///
4143
/// If [typeHelpers] is not provided, two built-in helpers are used:
4244
/// [JsonHelper] and [DateTimeHelper].
43-
const JsonSerializableGenerator({List<TypeHelper> typeHelpers})
44-
: this._typeHelpers = typeHelpers ?? _defaultHelpers;
45+
const JsonSerializableGenerator(
46+
{List<TypeHelper> typeHelpers, bool useWrappers: false})
47+
: this.useWrappers = useWrappers ?? false,
48+
this._typeHelpers = typeHelpers ?? _defaultHelpers;
4549

4650
/// Creates an instance of [JsonSerializableGenerator].
4751
///
4852
/// [typeHelpers] provides a set of [TypeHelper] that will be used along with
4953
/// the built-in helpers: [JsonHelper] and [DateTimeHelper].
5054
factory JsonSerializableGenerator.withDefaultHelpers(
51-
Iterable<TypeHelper> typeHelpers) =>
55+
Iterable<TypeHelper> typeHelpers,
56+
{bool useWrappers: false}) =>
5257
new JsonSerializableGenerator(
58+
useWrappers: useWrappers,
5359
typeHelpers: new List.unmodifiable(
5460
[typeHelpers, _defaultHelpers].expand((e) => e)));
5561

@@ -125,6 +131,7 @@ class JsonSerializableGenerator
125131

126132
if (classAnnotation.createToJson) {
127133
var mixClassName = '${prefix}SerializerMixin';
134+
var helpClassName = '${prefix}JsonMapWrapper';
128135

129136
//
130137
// Generate the mixin class
@@ -138,26 +145,96 @@ class JsonSerializableGenerator
138145
buffer.writeln(' ${field.type} get ${field.name};');
139146
}
140147

141-
buffer.writeln(' Map<String, dynamic> toJson() ');
148+
buffer.write(' Map<String, dynamic> toJson() ');
142149

143150
var writeNaive =
144151
fieldsList.every((e) => _writeJsonValueNaive(e, classAnnotation));
145152

146-
if (writeNaive) {
147-
// write simple `toJson` method that includes all keys...
148-
_writeToJsonSimple(buffer, fields.values, classAnnotation.nullable);
153+
if (useWrappers) {
154+
buffer.writeln('=> new $helpClassName(this);');
149155
} else {
150-
// At least one field should be excluded if null
151-
_writeToJsonWithNullChecks(buffer, fields.values, classAnnotation);
156+
if (writeNaive) {
157+
// write simple `toJson` method that includes all keys...
158+
_writeToJsonSimple(buffer, fields.values, classAnnotation.nullable);
159+
} else {
160+
// At least one field should be excluded if null
161+
_writeToJsonWithNullChecks(buffer, fields.values, classAnnotation);
162+
}
152163
}
153164

154165
// end of the mixin class
155166
buffer.writeln('}');
167+
168+
if (useWrappers) {
169+
_writeWrapper(
170+
buffer, helpClassName, mixClassName, classAnnotation, fields);
171+
}
156172
}
157173

158174
return buffer.toString();
159175
}
160176

177+
void _writeWrapper(
178+
StringBuffer buffer,
179+
String helpClassName,
180+
String mixClassName,
181+
JsonSerializable classAnnotation,
182+
Map<String, FieldElement> fields) {
183+
buffer.writeln();
184+
// TODO(kevmoo): write JsonMapWrapper if annotation lib is prefix-imported
185+
buffer.writeln('''class $helpClassName extends \$JsonMapWrapper {
186+
final $mixClassName _v;
187+
$helpClassName(this._v);
188+
''');
189+
190+
if (fields.values.every((e) => _writeJsonValueNaive(e, classAnnotation))) {
191+
// TODO(kevmoo): consider just doing one code path – if it's fast
192+
// enough
193+
var jsonKeys = fields.values.map(_safeNameAccess).join(', ');
194+
195+
// TODO(kevmoo): maybe put this in a static field instead?
196+
// const lists have unfortunate overhead
197+
buffer.writeln(''' @override
198+
Iterable<String> get keys => const [${jsonKeys}];
199+
''');
200+
} else {
201+
// At least one field should be excluded if null
202+
buffer.writeln('@override\nIterable<String> get keys sync* {');
203+
204+
for (var field in fields.values) {
205+
var nullCheck = !_writeJsonValueNaive(field, classAnnotation);
206+
if (nullCheck) {
207+
buffer.writeln('if (_v.${field.name} != null) {');
208+
}
209+
buffer.writeln('yield ${_safeNameAccess(field)};');
210+
if (nullCheck) {
211+
buffer.writeln('}');
212+
}
213+
}
214+
215+
buffer.writeln('}\n');
216+
}
217+
218+
buffer.writeln('''@override
219+
dynamic operator [](Object key) {
220+
if (key is String) {
221+
switch(key) {
222+
''');
223+
224+
for (var field in fields.values) {
225+
var valueAccess = '_v.${field.name}';
226+
buffer.write('''case ${_safeNameAccess(field)}:
227+
return ${_serializeField(field, classAnnotation.nullable, accessOverride: valueAccess)};''');
228+
}
229+
230+
buffer.writeln('''
231+
}}
232+
return null;
233+
}''');
234+
235+
buffer.writeln('}');
236+
}
237+
161238
void _writeToJsonWithNullChecks(StringBuffer buffer,
162239
Iterable<FieldElement> fields, JsonSerializable classAnnotation) {
163240
buffer.writeln('{');
@@ -352,7 +429,8 @@ void $toJsonMapHelperName(String key, dynamic value) {
352429
/// representing the serialization of a value.
353430
String _serialize(DartType targetType, String expression, bool nullable) =>
354431
_allHelpers
355-
.map((h) => h.serialize(targetType, expression, nullable, _serialize))
432+
.map((h) =>
433+
h.serialize(targetType, expression, nullable, _helperContext))
356434
.firstWhere((r) => r != null,
357435
orElse: () => throw new UnsupportedTypeError(
358436
targetType, expression, _notSupportedWithTypeHelpersMsg));
@@ -374,10 +452,35 @@ void $toJsonMapHelperName(String key, dynamic value) {
374452
String _deserialize(DartType targetType, String expression, bool nullable) =>
375453
_allHelpers
376454
.map((th) =>
377-
th.deserialize(targetType, expression, nullable, _deserialize))
455+
th.deserialize(targetType, expression, nullable, _helperContext))
378456
.firstWhere((r) => r != null,
379457
orElse: () => throw new UnsupportedTypeError(
380458
targetType, expression, _notSupportedWithTypeHelpersMsg));
459+
460+
_TypeHelperContext get _helperContext => _typeHelperContextExpando[this] ??=
461+
new _TypeHelperContext(_serialize, _deserialize, useWrappers);
462+
}
463+
464+
final _typeHelperContextExpando = new Expando<_TypeHelperContext>();
465+
466+
typedef String _TypeHelperGenerator(
467+
DartType fieldType, String expression, bool nullable);
468+
469+
class _TypeHelperContext implements SerializeContext, DeserializeContext {
470+
final _TypeHelperGenerator _serialize, _deserialize;
471+
472+
@override
473+
final bool useWrappers;
474+
475+
_TypeHelperContext(this._serialize, this._deserialize, this.useWrappers);
476+
477+
@override
478+
String serialize(DartType fieldType, String expression, bool nullable) =>
479+
_serialize(fieldType, expression, nullable);
480+
481+
@override
482+
String deserialize(DartType fieldType, String expression, bool nullable) =>
483+
_deserialize(fieldType, expression, nullable);
381484
}
382485

383486
String _safeNameAccess(FieldElement field) {

json_serializable/lib/src/type_helper.dart

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,14 @@ List<DartType> typeArgumentsOf(DartType type, TypeChecker checker) {
1515
return implementation?.typeArguments;
1616
}
1717

18-
typedef String TypeHelperGenerator(
19-
DartType fieldType, String expression, bool nullable);
18+
abstract class SerializeContext {
19+
bool get useWrappers;
20+
String serialize(DartType fieldType, String expression, bool nullable);
21+
}
22+
23+
abstract class DeserializeContext {
24+
String deserialize(DartType fieldType, String expression, bool nullable);
25+
}
2026

2127
abstract class TypeHelper {
2228
const TypeHelper();
@@ -36,9 +42,9 @@ abstract class TypeHelper {
3642
/// String serialize(DartType targetType, String expression) =>
3743
/// "$expression.id";
3844
/// ```.
39-
// TODO(kevmoo) – document `serializeNested`
45+
// TODO(kevmoo) – document `context`
4046
String serialize(DartType targetType, String expression, bool nullable,
41-
TypeHelperGenerator serializeNested);
47+
SerializeContext context);
4248

4349
/// Returns Dart code that deserializes an [expression] representing a JSON
4450
/// literal to into [targetType].
@@ -63,9 +69,9 @@ abstract class TypeHelper {
6369
/// String deserialize(DartType targetType, String expression) =>
6470
/// "new ${targetType.name}.fromInt($expression)";
6571
/// ```.
66-
// TODO(kevmoo) – document `deserializeNested`
72+
// TODO(kevmoo) – document `context`
6773
String deserialize(DartType targetType, String expression, bool nullable,
68-
TypeHelperGenerator deserializeNested);
74+
DeserializeContext context);
6975
}
7076

7177
/// A [TypeChecker] for [String], [bool] and [num].

json_serializable/lib/src/type_helpers/enum_helper.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ class EnumHelper extends TypeHelper {
77

88
@override
99
String serialize(DartType targetType, String expression, bool nullable,
10-
TypeHelperGenerator serializeNested) {
10+
SerializeContext context) {
1111
if (targetType is InterfaceType && targetType.element.isEnum) {
1212
return commonNullPrefix(
1313
nullable, expression, "$expression.toString().split('.')[1]");
@@ -18,7 +18,7 @@ class EnumHelper extends TypeHelper {
1818

1919
@override
2020
String deserialize(DartType targetType, String expression, bool nullable,
21-
TypeHelperGenerator deserializeNested) {
21+
DeserializeContext context) {
2222
if (targetType is InterfaceType && targetType.element.isEnum) {
2323
return commonNullPrefix(
2424
nullable,

0 commit comments

Comments
 (0)