Skip to content

Commit b230f78

Browse files
authored
Merge pull request #296 from davidmorgan/pretty-json-polymorphism
Support polymorphism in StandardJsonPlugin.
2 parents b2f3565 + 770c36e commit b230f78

File tree

5 files changed

+465
-119
lines changed

5 files changed

+465
-119
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
- Add `@BuiltValueEnum` and `@BuiltValueEnumConst` annotations for specifying
2020
settings for enums. Add `wireName` field to these to override the wire names
2121
in enums when serializing.
22+
- Add support for polymorphism to `StandardJsonPlugin`. It will now specify
23+
type names as needed via a `discriminator` field, which by defualt is
24+
called `$`. This can be changed in the `StandardJsonPlugin` constructor.
2225

2326
## 4.4.1
2427

built_value/lib/standard_json_plugin.dart

Lines changed: 142 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,25 @@ import 'dart:convert' show JSON;
55

66
/// Switches to "standard" JSON format.
77
///
8-
/// The default serialization format is more powerful, supporting polymorphism
9-
/// and more collection types. But, you may need to interact with other systems
10-
/// that use simple map-based JSON. If so, use [SerializersBuilder.addPlugin]
11-
/// to install this plugin.
8+
/// The default serialization format is more powerful, with better performance
9+
/// and support for more collection types. But, you may need to interact with
10+
/// other systems that use simple map-based JSON. If so, use
11+
/// [SerializersBuilder.addPlugin] to install this plugin.
1212
class StandardJsonPlugin implements SerializerPlugin {
1313
static final BuiltSet<Type> _unsupportedTypes =
1414
new BuiltSet<Type>([BuiltSet, BuiltListMultimap, BuiltSetMultimap]);
1515

16+
/// The field used to specify the value type if needed. Defaults to `$`.
17+
final String discriminator;
18+
19+
// The key used when there is just a single value, for example if serializing
20+
// an `int`.
21+
final String valueKey;
22+
23+
StandardJsonPlugin({this.discriminator: r'$', this.valueKey: ''});
24+
1625
@override
1726
Object beforeSerialize(Object object, FullType specifiedType) {
18-
if (specifiedType.isUnspecified)
19-
throw new ArgumentError('Standard JSON requires specifiedType.');
2027
if (_unsupportedTypes.contains(specifiedType.root)) {
2128
throw new ArgumentError(
2229
'Standard JSON cannot serialize type ${specifiedType.root}.');
@@ -26,58 +33,170 @@ class StandardJsonPlugin implements SerializerPlugin {
2633

2734
@override
2835
Object afterSerialize(Object object, FullType specifiedType) {
29-
return object is List &&
30-
specifiedType.root != BuiltList &&
31-
specifiedType.root != JsonObject
32-
? _toMap(object, _alreadyHasStringKeys(specifiedType))
33-
: object;
36+
if (object is List &&
37+
specifiedType.root != BuiltList &&
38+
specifiedType.root != JsonObject) {
39+
if (specifiedType.isUnspecified) {
40+
return _toMapWithDiscriminator(object);
41+
} else {
42+
return _toMap(object, _needsEncodedKeys(specifiedType));
43+
}
44+
} else {
45+
return object;
46+
}
3447
}
3548

3649
@override
3750
Object beforeDeserialize(Object object, FullType specifiedType) {
38-
return object is Map && specifiedType.root != JsonObject
39-
? _toList(object, _alreadyHasStringKeys(specifiedType))
40-
: object;
51+
if (object is Map && specifiedType.root != JsonObject) {
52+
if (specifiedType.isUnspecified) {
53+
return _toListUsingDiscriminator(object);
54+
} else {
55+
return _toList(object, _needsEncodedKeys(specifiedType));
56+
}
57+
} else {
58+
return object;
59+
}
4160
}
4261

4362
@override
4463
Object afterDeserialize(Object object, FullType specifiedType) {
4564
return object;
4665
}
4766

48-
bool _alreadyHasStringKeys(FullType specifiedType) =>
49-
specifiedType.root != BuiltMap ||
50-
specifiedType.parameters[0].root == String;
67+
/// Returns whether a type has keys that aren't supported by JSON maps; this
68+
/// only applies to `BuiltMap` with non-String keys.
69+
bool _needsEncodedKeys(FullType specifiedType) =>
70+
specifiedType.root == BuiltMap &&
71+
specifiedType.parameters[0].root != String;
5172

52-
Map _toMap(List list, bool alreadyStringKeys) {
73+
/// Converts serialization output, a `List`, to a `Map`, when the serialized
74+
/// type is known statically.
75+
Map _toMap(List list, bool needsEncodedKeys) {
5376
final result = {};
5477
for (int i = 0; i != list.length ~/ 2; ++i) {
5578
final key = list[i * 2];
5679
final value = list[i * 2 + 1];
57-
result[alreadyStringKeys ? key : _toStringKey(key)] = value;
80+
result[needsEncodedKeys ? _encodeKey(key) : key] = value;
5881
}
5982
return result;
6083
}
6184

62-
String _toStringKey(Object key) {
85+
/// Converts serialization output, a `List`, to a `Map`, when the serialized
86+
/// type is not known statically. The type will be specified in the
87+
/// [discriminator] field.
88+
Map _toMapWithDiscriminator(List list) {
89+
var type = list[0];
90+
91+
// Length is at least two because we have one entry for type and one for
92+
// the value.
93+
if (list.length == 2) {
94+
// Just a type and a primitive value. Encode the value in the map.
95+
return <Object, Object>{discriminator: type, valueKey: list[1]};
96+
}
97+
98+
if (type == 'list') {
99+
// Embed the list in the map.
100+
return <Object, Object>{discriminator: type, valueKey: list.sublist(1)};
101+
}
102+
103+
// If a map has non-String keys then they need encoding to strings before
104+
// it can be converted to JSON. Because we don't know the type, we also
105+
// won't know the type on deserialization, and signal this by changing the
106+
// type name on the wire to `encoded_map`.
107+
var needToEncodeKeys = false;
108+
if (type == 'map') {
109+
for (int i = 0; i != (list.length - 1) ~/ 2; ++i) {
110+
if (list[i * 2 + 1] is! String) {
111+
needToEncodeKeys = true;
112+
type = 'encoded_map';
113+
break;
114+
}
115+
}
116+
}
117+
118+
final result = <Object, Object>{discriminator: type};
119+
for (int i = 0; i != (list.length - 1) ~/ 2; ++i) {
120+
final key =
121+
needToEncodeKeys ? _encodeKey(list[i * 2 + 1]) : list[i * 2 + 1];
122+
final value = list[i * 2 + 2];
123+
result[key] = value;
124+
}
125+
return result;
126+
}
127+
128+
/// JSON-encodes an `Object` key so it can be stored as a `String`. Needed
129+
/// because JSON maps are only allowed strings as keys.
130+
String _encodeKey(Object key) {
63131
return JSON.encode(key);
64132
}
65133

66-
List _toList(Map map, bool alreadyStringKeys) {
134+
/// Converts [StandardJsonPlugin] serialization output, a `Map`, to a `List`,
135+
/// when the serialized type is known statically.
136+
List _toList(Map map, bool hasEncodedKeys) {
67137
final result = new List(map.length * 2);
68138
var i = 0;
69139
map.forEach((key, value) {
70140
// Drop null values, they are represented by missing keys.
71141
if (value == null) return;
72142

73-
result[i] = alreadyStringKeys ? key : _fromStringKey(key as String);
143+
result[i] = hasEncodedKeys ? _decodeKey(key as String) : key;
144+
result[i + 1] = value;
145+
i += 2;
146+
});
147+
return result;
148+
}
149+
150+
/// Converts [StandardJsonPlugin] serialization output, a `Map`, to a `List`,
151+
/// when the serialized type is not known statically. The type is retrieved
152+
/// from the [discriminator] field.
153+
List _toListUsingDiscriminator(Map map) {
154+
var type = map[discriminator];
155+
156+
if (type == null) {
157+
throw new ArgumentError('Unknown type on deserialization. '
158+
'Need either specifiedType or discriminator field.');
159+
}
160+
161+
if (type == 'list') {
162+
return [type]..addAll(map[valueKey] as Iterable);
163+
}
164+
165+
if (map.containsKey(valueKey)) {
166+
// Just a type and a primitive value. Retrieve the value in the map.
167+
final result = new List(2);
168+
result[0] = type;
169+
result[1] = map[valueKey];
170+
return result;
171+
}
172+
173+
// A type name of `encoded_map` indicates that the map has non-String keys
174+
// that have been serialized and JSON-encoded; decode the keys when
175+
// converting back to a `List`.
176+
final needToDecodeKeys = type == 'encoded_map';
177+
if (needToDecodeKeys) {
178+
type = 'map';
179+
}
180+
181+
final result = new List(map.length * 2 - 1);
182+
result[0] = type;
183+
184+
var i = 1;
185+
map.forEach((key, value) {
186+
if (key == discriminator) return;
187+
188+
// Drop null values, they are represented by missing keys.
189+
if (value == null) return;
190+
191+
result[i] = needToDecodeKeys ? _decodeKey(key as String) : key;
74192
result[i + 1] = value;
75193
i += 2;
76194
});
77195
return result;
78196
}
79197

80-
Object _fromStringKey(String key) {
198+
/// JSON-decodes a `String` encoded using [_encodeKey].
199+
Object _decodeKey(String key) {
81200
return JSON.decode(key);
82201
}
83202
}

0 commit comments

Comments
 (0)