-
Notifications
You must be signed in to change notification settings - Fork 183
/
standard_json_plugin.dart
227 lines (197 loc) · 7.72 KB
/
standard_json_plugin.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
// Copyright (c) 2015, Google Inc. Please see the AUTHORS file for details.
// All rights reserved. Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
import 'package:built_collection/built_collection.dart';
import 'package:built_value/json_object.dart';
import 'package:built_value/serializer.dart';
import 'dart:convert' show json;
/// Switches to "standard" JSON format.
///
/// The default serialization format is more powerful, with better performance
/// and support for more collection types. But, you may need to interact with
/// other systems that use simple map-based JSON. If so, use
/// [SerializersBuilder.addPlugin] to install this plugin.
///
/// When using this plugin you may wish to also install
/// `Iso8601DateTimeSerializer` which switches serialization of `DateTime`
/// from microseconds since epoch to ISO 8601 format.
class StandardJsonPlugin implements SerializerPlugin {
static final BuiltSet<Type> _unsupportedTypes =
BuiltSet<Type>([BuiltListMultimap, BuiltSetMultimap]);
/// The field used to specify the value type if needed. Defaults to `$`.
final String discriminator;
/// The key used when there is just a single value, for example if serializing
/// an `int`.
final String valueKey;
/// The types to leave as a list when converting into json.
final BuiltSet<Type> typesToLeaveAsList;
StandardJsonPlugin({
this.discriminator = r'$',
this.valueKey = '',
Iterable<Type>? typesToLeaveAsList,
}) : typesToLeaveAsList = BuiltSet<Type>(
{BuiltList, BuiltSet, JsonObject, ...?typesToLeaveAsList});
@override
Object? beforeSerialize(Object? object, FullType specifiedType) {
if (_unsupportedTypes.contains(specifiedType.root)) {
throw ArgumentError(
'Standard JSON cannot serialize type ${specifiedType.root}.');
}
return object;
}
@override
Object? afterSerialize(Object? object, FullType specifiedType) {
if (object is List && !typesToLeaveAsList.contains(specifiedType.root)) {
if (specifiedType.isUnspecified) {
return _toMapWithDiscriminator(object);
} else {
return _toMap(object, _needsEncodedKeys(specifiedType));
}
} else {
return object;
}
}
@override
Object? beforeDeserialize(Object? object, FullType specifiedType) {
if (object is Map && specifiedType.root != JsonObject) {
if (specifiedType.isUnspecified) {
return _toListUsingDiscriminator(object);
} else {
return _toList(object, _needsEncodedKeys(specifiedType),
keepNulls: specifiedType.root == BuiltMap);
}
} else {
return object;
}
}
@override
Object? afterDeserialize(Object? object, FullType specifiedType) {
return object;
}
/// Returns whether a type has keys that aren't supported by JSON maps; this
/// only applies to `BuiltMap` with non-String keys.
bool _needsEncodedKeys(FullType specifiedType) =>
specifiedType.root == BuiltMap &&
specifiedType.parameters[0].root != String;
/// Converts serialization output, a `List`, to a `Map`, when the serialized
/// type is known statically.
Map _toMap(List list, bool needsEncodedKeys) {
var result = <String, Object?>{};
for (var i = 0; i != list.length ~/ 2; ++i) {
final key = list[i * 2];
final value = list[i * 2 + 1];
result[needsEncodedKeys ? _encodeKey(key) : key as String] = value;
}
return result;
}
/// Converts serialization output, a `List`, to a `Map`, when the serialized
/// type is not known statically. The type will be specified in the
/// [discriminator] field.
Map _toMapWithDiscriminator(List list) {
var type = list[0];
if (type == 'list') {
// Embed the list in the map.
return <String, Object>{discriminator: type, valueKey: list.sublist(1)};
}
// Length is at least two because we have one entry for type and one for
// the value.
if (list.length == 2) {
// Just a type and a primitive value. Encode the value in the map.
return <String, Object?>{discriminator: type, valueKey: list[1]};
}
// If a map has non-String keys then they need encoding to strings before
// it can be converted to JSON. Because we don't know the type, we also
// won't know the type on deserialization, and signal this by changing the
// type name on the wire to `encoded_map`.
var needToEncodeKeys = false;
if (type == 'map') {
for (var i = 0; i != (list.length - 1) ~/ 2; ++i) {
if (list[i * 2 + 1] is! String) {
needToEncodeKeys = true;
type = 'encoded_map';
break;
}
}
}
var result = <String, Object>{discriminator: type};
for (var i = 0; i != (list.length - 1) ~/ 2; ++i) {
final key = needToEncodeKeys
? _encodeKey(list[i * 2 + 1])
: list[i * 2 + 1] as String;
final value = list[i * 2 + 2];
result[key] = value;
}
return result;
}
/// JSON-encodes an `Object` key so it can be stored as a `String`. Needed
/// because JSON maps are only allowed strings as keys.
String _encodeKey(Object key) {
return json.encode(key);
}
/// Converts [StandardJsonPlugin] serialization output, a `Map`, to a `List`,
/// when the serialized type is known statically.
///
/// By default keys with null values are dropped, pass [keepNulls] true when
/// the map is an actual map with nullable values, so they should be kept.
List<Object?> _toList(Map map, bool hasEncodedKeys,
{bool keepNulls = false}) {
var nullValueCount =
keepNulls ? 0 : map.values.where((value) => value == null).length;
var result = List<Object?>.filled(
(map.length - nullValueCount) * 2, 0 /* Will be overwritten. */);
var i = 0;
map.forEach((key, value) {
// Drop null values, they are represented by missing keys.
if (!keepNulls && value == null) return;
result[i] = hasEncodedKeys ? _decodeKey(key as String) : key;
result[i + 1] = value;
i += 2;
});
return result;
}
/// Converts [StandardJsonPlugin] serialization output, a `Map`, to a `List`,
/// when the serialized type is not known statically. The type is retrieved
/// from the [discriminator] field.
List<Object?> _toListUsingDiscriminator(Map map) {
var type = map[discriminator];
if (type == null) {
throw ArgumentError('Unknown type on deserialization. '
'Need either specifiedType or discriminator field.');
}
if (type == 'list') {
return [type, ...(map[valueKey] as Iterable)];
}
if (map.containsKey(valueKey)) {
// Just a type and a primitive value. Retrieve the value in the map.
final result = List<Object?>.filled(2, 0 /* Will be overwritten. */);
result[0] = type;
result[1] = map[valueKey];
return result;
}
// A type name of `encoded_map` indicates that the map has non-String keys
// that have been serialized and JSON-encoded; decode the keys when
// converting back to a `List`.
var needToDecodeKeys = type == 'encoded_map';
if (needToDecodeKeys) {
type = 'map';
}
var nullValueCount = map.values.where((value) => value == null).length;
var result = List<Object>.filled(
(map.length - nullValueCount) * 2 - 1, 0 /* Will be overwritten. */);
result[0] = type;
var i = 1;
map.forEach((key, value) {
if (key == discriminator) return;
// Drop null values, they are represented by missing keys.
if (value == null) return;
result[i] = needToDecodeKeys ? _decodeKey(key as String) : key;
result[i + 1] = value;
i += 2;
});
return result;
}
/// JSON-decodes a `String` encoded using [_encodeKey].
Object? _decodeKey(String key) {
return json.decode(key);
}
}