// 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 _unsupportedTypes = BuiltSet([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 typesToLeaveAsList; StandardJsonPlugin({ this.discriminator = r'$', this.valueKey = '', Iterable? typesToLeaveAsList, }) : typesToLeaveAsList = BuiltSet( {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 = {}; 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 {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 {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 = {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 _toList(Map map, bool hasEncodedKeys, {bool keepNulls = false}) { var nullValueCount = keepNulls ? 0 : map.values.where((value) => value == null).length; var result = List.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 _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.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.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); } }