@@ -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.
1212class 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