// Copyright (c) 2013, the Dart project authors. 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. part of "dart:convert"; /// Error thrown by JSON serialization if an object cannot be serialized. /// /// The [unsupportedObject] field holds that object that failed to be serialized. /// /// If an object isn't directly serializable, the serializer calls the `toJson` /// method on the object. If that call fails, the error will be stored in the /// [cause] field. If the call returns an object that isn't directly /// serializable, the [cause] is null. class JsonUnsupportedObjectError extends Error { /// The object that could not be serialized. final Object? unsupportedObject; /// The exception thrown when trying to convert the object. final Object? cause; /// The partial result of the conversion, up until the error happened. /// /// May be null. final String? partialResult; JsonUnsupportedObjectError( this.unsupportedObject, { this.cause, this.partialResult, }); String toString() { var safeString = Error.safeToString(unsupportedObject); String prefix; if (cause != null) { prefix = "Converting object to an encodable object failed:"; } else { prefix = "Converting object did not return an encodable object:"; } return "$prefix $safeString"; } } /// Reports that an object could not be stringified due to cyclic references. /// /// An object that references itself cannot be serialized by /// [JsonCodec.encode]/[JsonEncoder.convert]. /// When the cycle is detected, a [JsonCyclicError] is thrown. class JsonCyclicError extends JsonUnsupportedObjectError { /// The first object that was detected as part of a cycle. JsonCyclicError(Object? object) : super(object); String toString() => "Cyclic error in JSON stringify"; } /// An instance of the default implementation of the [JsonCodec]. /// /// This instance provides a convenient access to the most common JSON /// use cases. /// /// Examples: /// ```dart /// var encoded = json.encode([1, 2, { "a": null }]); /// var decoded = json.decode('["foo", { "bar": 499 }]'); /// ``` /// The top-level [jsonEncode] and [jsonDecode] functions may be used instead if /// a local variable shadows the [json] constant. const JsonCodec json = JsonCodec(); /// Converts [object] to a JSON string. /// /// If value contains objects that are not directly encodable to a JSON /// string (a value that is not a number, boolean, string, null, list or a map /// with string keys), the [toEncodable] function is used to convert it to an /// object that must be directly encodable. /// /// If [toEncodable] is omitted, it defaults to a function that returns the /// result of calling `.toJson()` on the unencodable object. /// /// Shorthand for `json.encode`. Useful if a local variable shadows the global /// [json] constant. /// /// Example: /// ```dart /// const data = {'text': 'foo', 'value': 2, 'status': false, 'extra': null}; /// final String jsonString = jsonEncode(data); /// print(jsonString); // {"text":"foo","value":2,"status":false,"extra":null} /// ``` /// /// Example of converting an otherwise unsupported object to a /// custom JSON format: /// /// ```dart /// class CustomClass { /// final String text; /// final int value; /// CustomClass({required this.text, required this.value}); /// CustomClass.fromJson(Map json) /// : text = json['text'], /// value = json['value']; /// /// static Map toJson(CustomClass value) => /// {'text': value.text, 'value': value.value}; /// } /// /// void main() { /// final CustomClass cc = CustomClass(text: 'Dart', value: 123); /// final jsonText = jsonEncode({'cc': cc}, /// toEncodable: (Object? value) => value is CustomClass /// ? CustomClass.toJson(value) /// : throw UnsupportedError('Cannot convert to JSON: $value')); /// print(jsonText); // {"cc":{"text":"Dart","value":123}} /// } /// ``` String jsonEncode( Object? object, { Object? toEncodable(Object? nonEncodable)?, }) => json.encode(object, toEncodable: toEncodable); /// Parses the string and returns the resulting Json object. /// /// The optional [reviver] function is called once for each object or list /// property that has been parsed during decoding. The `key` argument is either /// the integer list index for a list property, the string map key for object /// properties, or `null` for the final result. /// /// The default [reviver] (when not provided) is the identity function. /// /// Shorthand for `json.decode`. Useful if a local variable shadows the global /// [json] constant. /// /// Example: /// ```dart /// const jsonString = /// '{"text": "foo", "value": 1, "status": false, "extra": null}'; /// /// final data = jsonDecode(jsonString); /// print(data['text']); // foo /// print(data['value']); // 1 /// print(data['status']); // false /// print(data['extra']); // null /// /// const jsonArray = ''' /// [{"text": "foo", "value": 1, "status": true}, /// {"text": "bar", "value": 2, "status": false}] /// '''; /// /// final List dataList = jsonDecode(jsonArray); /// print(dataList[0]); // {text: foo, value: 1, status: true} /// print(dataList[1]); // {text: bar, value: 2, status: false} /// /// final item = dataList[0]; /// print(item['text']); // foo /// print(item['value']); // 1 /// print(item['status']); // false /// ``` dynamic jsonDecode( String source, { Object? reviver(Object? key, Object? value)?, }) => json.decode(source, reviver: reviver); /// A [JsonCodec] encodes JSON objects to strings and decodes strings to /// JSON objects. /// /// Examples: /// ```dart /// var encoded = json.encode([1, 2, { "a": null }]); /// var decoded = json.decode('["foo", { "bar": 499 }]'); /// ``` final class JsonCodec extends Codec { final Object? Function(Object? key, Object? value)? _reviver; final Object? Function(dynamic)? _toEncodable; /// Creates a `JsonCodec` with the given reviver and encoding function. /// /// The [reviver] function is called during decoding. It is invoked once for /// each object or list property that has been parsed. /// The `key` argument is either the integer list index for a list property, /// the string map key for object properties, or `null` for the final result. /// /// If [reviver] is omitted, it defaults to returning the value argument. /// /// The [toEncodable] function is used during encoding. It is invoked for /// values that are not directly encodable to a string (a value that is not a /// number, boolean, string, null, list or a map with string keys). The /// function must return an object that is directly encodable. The elements of /// a returned list and values of a returned map do not need to be directly /// encodable, and if they aren't, `toEncodable` will be used on them as well. /// Please notice that it is possible to cause an infinite recursive regress /// in this way, by effectively creating an infinite data structure through /// repeated call to `toEncodable`. /// /// If [toEncodable] is omitted, it defaults to a function that returns the /// result of calling `.toJson()` on the unencodable object. const JsonCodec({ Object? reviver(Object? key, Object? value)?, Object? toEncodable(dynamic object)?, }) : _reviver = reviver, _toEncodable = toEncodable; /// Creates a `JsonCodec` with the given reviver. /// /// The [reviver] function is called once for each object or list property /// that has been parsed during decoding. The `key` argument is either the /// integer list index for a list property, the string map key for object /// properties, or `null` for the final result. JsonCodec.withReviver(dynamic reviver(Object? key, Object? value)) : this(reviver: reviver); /// Parses the string and returns the resulting Json object. /// /// The optional [reviver] function is called once for each object or list /// property that has been parsed during decoding. The `key` argument is either /// the integer list index for a list property, the string map key for object /// properties, or `null` for the final result. /// /// The default [reviver] (when not provided) is the identity function. dynamic decode( String source, { Object? reviver(Object? key, Object? value)?, }) { reviver ??= _reviver; if (reviver == null) return decoder.convert(source); return JsonDecoder(reviver).convert(source); } /// Converts [value] to a JSON string. /// /// If value contains objects that are not directly encodable to a JSON /// string (a value that is not a number, boolean, string, null, list or a map /// with string keys), the [toEncodable] function is used to convert it to an /// object that must be directly encodable. /// /// If [toEncodable] is omitted, it defaults to a function that returns the /// result of calling `.toJson()` on the unencodable object. String encode(Object? value, {Object? toEncodable(dynamic object)?}) { toEncodable ??= _toEncodable; if (toEncodable == null) return encoder.convert(value); return JsonEncoder(toEncodable).convert(value); } JsonEncoder get encoder { if (_toEncodable == null) return const JsonEncoder(); return JsonEncoder(_toEncodable); } JsonDecoder get decoder { if (_reviver == null) return const JsonDecoder(); return JsonDecoder(_reviver); } } /// This class converts JSON objects to strings. /// /// Example: /// /// ```dart /// const JsonEncoder encoder = JsonEncoder(); /// const data = {'text': 'foo', 'value': '2'}; /// /// final String jsonString = encoder.convert(data); /// print(jsonString); // {"text":"foo","value":"2"} /// ``` /// /// Example of pretty-printed output: /// /// ```dart /// const JsonEncoder encoder = JsonEncoder.withIndent(' '); /// /// const data = {'text': 'foo', 'value': '2'}; /// final String jsonString = encoder.convert(data); /// print(jsonString); /// // { /// // "text": "foo", /// // "value": "2" /// // } /// ``` final class JsonEncoder extends Converter { /// The string used for indention. /// /// When generating multi-line output, this string is inserted once at the /// beginning of each indented line for each level of indentation. /// /// If `null`, the output is encoded as a single line. final String? indent; /// Function called on non-encodable objects to return a replacement /// encodable object that will be encoded in the orignal's place. final Object? Function(dynamic)? _toEncodable; /// Creates a JSON encoder. /// /// The JSON encoder handles numbers, strings, booleans, null, lists and /// maps with string keys directly. /// /// Any other object is attempted converted by [toEncodable] to an /// object that is of one of the convertible types. /// /// If [toEncodable] is omitted, it defaults to calling `.toJson()` on /// the object. const JsonEncoder([Object? toEncodable(dynamic object)?]) : indent = null, _toEncodable = toEncodable; /// Creates a JSON encoder that creates multi-line JSON. /// /// The encoding of elements of lists and maps are indented and put on separate /// lines. The [indent] string is prepended to these elements, once for each /// level of indentation. /// /// If [indent] is `null`, the output is encoded as a single line. /// /// The JSON encoder handles numbers, strings, booleans, null, lists and /// maps with string keys directly. /// /// Any other object is attempted converted by [toEncodable] to an /// object that is of one of the convertible types. /// /// If [toEncodable] is omitted, it defaults to calling `.toJson()` on /// the object. const JsonEncoder.withIndent( this.indent, [ Object? toEncodable(dynamic object)?, ]) : _toEncodable = toEncodable; /// Converts [object] to a JSON [String]. /// /// Directly serializable values are [num], [String], [bool], and [Null], as /// well as some [List] and [Map] values. For [List], the elements must all be /// serializable. For [Map], the keys must be [String] and the values must be /// serializable. /// /// If a value of any other type is attempted to be serialized, the /// `toEncodable` function provided in the constructor is called with the value /// as argument. The result, which must be a directly serializable value, is /// serialized instead of the original value. /// /// If the conversion throws, or returns a value that is not directly /// serializable, a [JsonUnsupportedObjectError] exception is thrown. /// If the call throws, the error is caught and stored in the /// [JsonUnsupportedObjectError]'s `cause` field. /// /// If a [List] or [Map] contains a reference to itself, directly or through /// other lists or maps, it cannot be serialized and a [JsonCyclicError] is /// thrown. /// /// [object] should not change during serialization. /// /// If an object is serialized more than once, [convert] may cache the text /// for it. In other words, if the content of an object changes after it is /// first serialized, the new values may not be reflected in the result. String convert(Object? object) => _JsonStringStringifier.stringify(object, _toEncodable, indent); /// Starts a chunked conversion. /// /// The converter works more efficiently if the given [sink] is a /// [StringConversionSink]. /// /// Returns a chunked-conversion sink that accepts at most one object. It is /// an error to invoke `add` more than once on the returned sink. ChunkedConversionSink startChunkedConversion(Sink sink) { if (sink is _Utf8EncoderSink) { return _JsonUtf8EncoderSink( sink._sink, _toEncodable, JsonUtf8Encoder._utf8Encode(indent), JsonUtf8Encoder._defaultBufferSize, ); } return _JsonEncoderSink( sink is StringConversionSink ? sink : StringConversionSink.from(sink), _toEncodable, indent, ); } // Override the base class's bind, to provide a better type. Stream bind(Stream stream) => super.bind(stream); Converter fuse(Converter other) { if (other is Utf8Encoder) { // The instance check guarantees that `T` is (a subtype of) List, // but the static type system doesn't know that, and so we cast. return JsonUtf8Encoder(indent, _toEncodable) as Converter; } return super.fuse(other); } } /// Encoder that encodes a single object as a UTF-8 encoded JSON string. /// /// This encoder works equivalently to first converting the object to /// a JSON string, and then UTF-8 encoding the string, but without /// creating an intermediate string. final class JsonUtf8Encoder extends Converter> { /// Default buffer size used by the JSON-to-UTF-8 encoder. static const int _defaultBufferSize = 256; /// Indentation used in pretty-print mode, `null` if not pretty. final List? _indent; /// Function called with each un-encodable object encountered. final Object? Function(dynamic)? _toEncodable; /// UTF-8 buffer size. final int _bufferSize; /// Create converter. /// /// If [indent] is non-`null`, the converter attempts to "pretty-print" the /// JSON, and uses `indent` as the indentation. Otherwise the result has no /// whitespace outside of string literals. /// If `indent` contains characters that are not valid JSON whitespace /// characters, the result will not be valid JSON. JSON whitespace characters /// are space (U+0020), tab (U+0009), line feed (U+000a) and carriage return /// (U+000d) ([ECMA /// 404](http://www.ecma-international.org/publications/standards/Ecma-404.htm)). /// /// The [bufferSize] is the size of the internal buffers used to collect /// UTF-8 code units. /// If using [startChunkedConversion], it will be the size of the chunks. /// /// The JSON encoder handles numbers, strings, booleans, null, lists and maps /// directly. /// /// Any other object is attempted converted by [toEncodable] to an object that /// is of one of the convertible types. /// /// If [toEncodable] is omitted, it defaults to calling `.toJson()` on the /// object. JsonUtf8Encoder([ String? indent, dynamic toEncodable(dynamic object)?, int? bufferSize, ]) : _indent = _utf8Encode(indent), _toEncodable = toEncodable, _bufferSize = bufferSize ?? _defaultBufferSize; static List? _utf8Encode(String? string) { if (string == null) return null; if (string.isEmpty) return Uint8List(0); checkAscii: { for (var i = 0; i < string.length; i++) { if (string.codeUnitAt(i) >= 0x80) break checkAscii; } return string.codeUnits; } return utf8.encode(string); } /// Convert [object] into UTF-8 encoded JSON. List convert(Object? object) { var bytes = >[]; // The `stringify` function always converts into chunks. // Collect the chunks into the `bytes` list, then combine them afterwards. void addChunk(Uint8List chunk, int start, int end) { if (start > 0 || end < chunk.length) { var length = end - start; chunk = Uint8List.view( chunk.buffer, chunk.offsetInBytes + start, length, ); } bytes.add(chunk); } _JsonUtf8Stringifier.stringify( object, _indent, _toEncodable, _bufferSize, addChunk, ); if (bytes.length == 1) return bytes[0]; var length = 0; for (var i = 0; i < bytes.length; i++) { length += bytes[i].length; } var result = Uint8List(length); for (var i = 0, offset = 0; i < bytes.length; i++) { var byteList = bytes[i]; int end = offset + byteList.length; result.setRange(offset, end, byteList); offset = end; } return result; } /// Start a chunked conversion. /// /// Only one object can be passed into the returned sink. /// /// The argument [sink] will receive byte lists in sizes depending on the /// `bufferSize` passed to the constructor when creating this encoder. ChunkedConversionSink startChunkedConversion(Sink> sink) { ByteConversionSink byteSink; if (sink is ByteConversionSink) { byteSink = sink; } else { byteSink = ByteConversionSink.from(sink); } return _JsonUtf8EncoderSink(byteSink, _toEncodable, _indent, _bufferSize); } // Override the base class's bind, to provide a better type. Stream> bind(Stream stream) { return super.bind(stream); } } /// Implements the chunked conversion from object to its JSON representation. /// /// The sink only accepts one value, but will produce output in a chunked way. class _JsonEncoderSink extends ChunkedConversionSink { final String? _indent; final Object? Function(dynamic)? _toEncodable; final StringConversionSink _sink; bool _isDone = false; _JsonEncoderSink(this._sink, this._toEncodable, this._indent); /// Encodes the given object [o]. /// /// It is an error to invoke this method more than once on any instance. While /// this makes the input effectively non-chunked the output will be generated /// in a chunked way. void add(Object? o) { if (_isDone) { throw StateError("Only one call to add allowed"); } _isDone = true; var stringSink = _sink.asStringSink(); _JsonStringStringifier.printOn(o, stringSink, _toEncodable, _indent); stringSink.close(); } void close() { /* do nothing */ } } /// Sink returned when starting a chunked conversion from object to bytes. class _JsonUtf8EncoderSink extends ChunkedConversionSink { /// The byte sink receiving the encoded chunks. final ByteConversionSink _sink; final List? _indent; final Object? Function(dynamic)? _toEncodable; final int _bufferSize; bool _isDone = false; _JsonUtf8EncoderSink( this._sink, this._toEncodable, this._indent, this._bufferSize, ); /// Callback called for each slice of result bytes. void _addChunk(Uint8List chunk, int start, int end) { _sink.addSlice(chunk, start, end, false); } void add(Object? object) { if (_isDone) { throw StateError("Only one call to add allowed"); } _isDone = true; _JsonUtf8Stringifier.stringify( object, _indent, _toEncodable, _bufferSize, _addChunk, ); _sink.close(); } void close() { if (!_isDone) { _isDone = true; _sink.close(); } } } /// This class parses JSON strings and builds the corresponding objects. /// /// A JSON input must be the JSON encoding of a single JSON value, /// which can be a list or map containing other values. /// /// Throws [FormatException] if the input is not valid JSON text. /// /// Example: /// ```dart /// const JsonDecoder decoder = JsonDecoder(); /// /// const String jsonString = ''' /// { /// "data": [{"text": "foo", "value": 1 }, /// {"text": "bar", "value": 2 }], /// "text": "Dart" /// } /// '''; /// /// final Map object = decoder.convert(jsonString); /// /// final item = object['data'][0]; /// print(item['text']); // foo /// print(item['value']); // 1 /// /// print(object['text']); // Dart /// ``` /// /// When used as a [StreamTransformer], the input stream may emit /// multiple strings. The concatenation of all of these strings must /// be a valid JSON encoding of a single JSON value. final class JsonDecoder extends Converter { final Object? Function(Object? key, Object? value)? _reviver; /// Constructs a new JsonDecoder. /// /// The [reviver] may be `null`. const JsonDecoder([Object? reviver(Object? key, Object? value)?]) : _reviver = reviver; /// Converts the given JSON-string [input] to its corresponding object. /// /// Parsed JSON values are of the types [num], [String], [bool], [Null], /// [List]s of parsed JSON values or [Map]s from [String] to parsed JSON /// values. /// /// If `this` was initialized with a reviver, then the parsing operation /// invokes the reviver on every object or list property that has been parsed. /// The arguments are the property name ([String]) or list index ([int]), and /// the value is the parsed value. The return value of the reviver is used as /// the value of that property instead the parsed value. /// /// Throws [FormatException] if the input is not valid JSON text. dynamic convert(String input) => _parseJson(input, _reviver); /// Starts a conversion from a chunked JSON string to its corresponding object. /// /// The output [sink] receives exactly one decoded element through `add`. external StringConversionSink startChunkedConversion(Sink sink); // Override the base class's bind, to provide a better type. Stream bind(Stream stream) => super.bind(stream); } // Internal optimized JSON parsing implementation. external dynamic _parseJson(String source, reviver(key, value)?); // Implementation of encoder/stringifier. // ignore: avoid_dynamic_calls dynamic _defaultToEncodable(dynamic object) => object.toJson(); /// JSON encoder that traverses an object structure and writes JSON source. /// /// This is an abstract implementation that doesn't decide on the output /// format, but writes the JSON through abstract methods like [writeString]. abstract class _JsonStringifier { // Character code constants. static const int backspace = 0x08; static const int tab = 0x09; static const int newline = 0x0a; static const int carriageReturn = 0x0d; static const int formFeed = 0x0c; static const int quote = 0x22; static const int char_0 = 0x30; static const int backslash = 0x5c; static const int char_b = 0x62; static const int char_d = 0x64; static const int char_f = 0x66; static const int char_n = 0x6e; static const int char_r = 0x72; static const int char_t = 0x74; static const int char_u = 0x75; static const int surrogateMin = 0xd800; static const int surrogateMask = 0xfc00; static const int surrogateLead = 0xd800; static const int surrogateTrail = 0xdc00; /// List of objects currently being traversed. Used to detect cycles. final List _seen = []; /// Function called for each un-encodable object encountered. final Function(dynamic) _toEncodable; _JsonStringifier(dynamic toEncodable(dynamic o)?) : _toEncodable = toEncodable ?? _defaultToEncodable; String? get _partialResult; /// Append a string to the JSON output. void writeString(String characters); /// Append part of a string to the JSON output. void writeStringSlice(String characters, int start, int end); /// Append a single character, given by its code point, to the JSON output. void writeCharCode(int charCode); /// Write a number to the JSON output. void writeNumber(num number); // ('0' + x) or ('a' + x - 10) static int hexDigit(int x) => x < 10 ? 48 + x : 87 + x; /// Write, and suitably escape, a string's content as a JSON string literal. void writeStringContent(String s) { var offset = 0; final length = s.length; for (var i = 0; i < length; i++) { var charCode = s.codeUnitAt(i); if (charCode > backslash) { if (charCode >= surrogateMin) { // Possible surrogate. Check if it is unpaired. if (((charCode & surrogateMask) == surrogateLead && !(i + 1 < length && (s.codeUnitAt(i + 1) & surrogateMask) == surrogateTrail)) || ((charCode & surrogateMask) == surrogateTrail && !(i - 1 >= 0 && (s.codeUnitAt(i - 1) & surrogateMask) == surrogateLead))) { // Lone surrogate. if (i > offset) writeStringSlice(s, offset, i); offset = i + 1; writeCharCode(backslash); writeCharCode(char_u); writeCharCode(char_d); writeCharCode(hexDigit((charCode >> 8) & 0xf)); writeCharCode(hexDigit((charCode >> 4) & 0xf)); writeCharCode(hexDigit(charCode & 0xf)); } } continue; } if (charCode < 32) { if (i > offset) writeStringSlice(s, offset, i); offset = i + 1; writeCharCode(backslash); switch (charCode) { case backspace: writeCharCode(char_b); break; case tab: writeCharCode(char_t); break; case newline: writeCharCode(char_n); break; case formFeed: writeCharCode(char_f); break; case carriageReturn: writeCharCode(char_r); break; default: writeCharCode(char_u); writeCharCode(char_0); writeCharCode(char_0); writeCharCode(hexDigit((charCode >> 4) & 0xf)); writeCharCode(hexDigit(charCode & 0xf)); break; } } else if (charCode == quote || charCode == backslash) { if (i > offset) writeStringSlice(s, offset, i); offset = i + 1; writeCharCode(backslash); writeCharCode(charCode); } } if (offset == 0) { writeString(s); } else if (offset < length) { writeStringSlice(s, offset, length); } } /// Check if an encountered object is already being traversed. /// /// Records the object if it isn't already seen. Should have a matching call to /// [_removeSeen] when the object is no longer being traversed. void _checkCycle(Object? object) { for (var i = 0; i < _seen.length; i++) { if (identical(object, _seen[i])) { throw JsonCyclicError(object); } } _seen.add(object); } /// Remove [object] from the list of currently traversed objects. /// /// Should be called in the opposite order of the matching [_checkCycle] /// calls. void _removeSeen(Object? object) { assert(_seen.isNotEmpty); assert(identical(_seen.last, object)); _seen.removeLast(); } /// Write an object. /// /// If [object] isn't directly encodable, the [_toEncodable] function gets one /// chance to return a replacement which is encodable. void writeObject(Object? object) { // Tries stringifying object directly. If it's not a simple value, List or // Map, call toJson() to get a custom representation and try serializing // that. if (writeJsonValue(object)) return; _checkCycle(object); try { var customJson = _toEncodable(object); if (!writeJsonValue(customJson)) { throw JsonUnsupportedObjectError(object, partialResult: _partialResult); } _removeSeen(object); } catch (e) { throw JsonUnsupportedObjectError( object, cause: e, partialResult: _partialResult, ); } } /// Serialize a [num], [String], [bool], [Null], [List] or [Map] value. /// /// Returns true if the value is one of these types, and false if not. /// If a value is both a [List] and a [Map], it's serialized as a [List]. bool writeJsonValue(Object? object) { if (object is num) { if (!object.isFinite) return false; writeNumber(object); return true; } else if (identical(object, true)) { writeString('true'); return true; } else if (identical(object, false)) { writeString('false'); return true; } else if (object == null) { writeString('null'); return true; } else if (object is String) { writeString('"'); writeStringContent(object); writeString('"'); return true; } else if (object is List) { _checkCycle(object); writeList(object); _removeSeen(object); return true; } else if (object is Map) { _checkCycle(object); // writeMap can fail if keys are not all strings. var success = writeMap(object); _removeSeen(object); return success; } else { return false; } } /// Serialize a [List]. void writeList(List list) { writeString('['); if (list.isNotEmpty) { writeObject(list[0]); for (var i = 1; i < list.length; i++) { writeString(','); writeObject(list[i]); } } writeString(']'); } /// Serialize a [Map]. bool writeMap(Map map) { if (map.isEmpty) { writeString("{}"); return true; } var keyValueList = List.filled(map.length * 2, null); var i = 0; var allStringKeys = true; map.forEach((key, value) { if (key is! String) { allStringKeys = false; } keyValueList[i++] = key; keyValueList[i++] = value; }); if (!allStringKeys) return false; writeString('{'); var separator = '"'; for (var i = 0; i < keyValueList.length; i += 2) { writeString(separator); separator = ',"'; writeStringContent(keyValueList[i] as String); writeString('":'); writeObject(keyValueList[i + 1]); } writeString('}'); return true; } } /// A modification of [_JsonStringifier] which indents the contents of [List] and /// [Map] objects using the specified indent value. /// /// Subclasses should implement [writeIndentation]. mixin _JsonPrettyPrintMixin implements _JsonStringifier { int _indentLevel = 0; /// Add [indentLevel] indentations to the JSON output. void writeIndentation(int indentLevel); void writeList(List list) { if (list.isEmpty) { writeString('[]'); } else { writeString('[\n'); _indentLevel++; writeIndentation(_indentLevel); writeObject(list[0]); for (var i = 1; i < list.length; i++) { writeString(',\n'); writeIndentation(_indentLevel); writeObject(list[i]); } writeString('\n'); _indentLevel--; writeIndentation(_indentLevel); writeString(']'); } } bool writeMap(Map map) { if (map.isEmpty) { writeString("{}"); return true; } var keyValueList = List.filled(map.length * 2, null); var i = 0; var allStringKeys = true; map.forEach((key, value) { if (key is! String) { allStringKeys = false; } keyValueList[i++] = key; keyValueList[i++] = value; }); if (!allStringKeys) return false; writeString('{\n'); _indentLevel++; var separator = ""; for (var i = 0; i < keyValueList.length; i += 2) { writeString(separator); separator = ",\n"; writeIndentation(_indentLevel); writeString('"'); writeStringContent(keyValueList[i] as String); writeString('": '); writeObject(keyValueList[i + 1]); } writeString('\n'); _indentLevel--; writeIndentation(_indentLevel); writeString('}'); return true; } } /// A specialization of [_JsonStringifier] that writes its JSON to a string. class _JsonStringStringifier extends _JsonStringifier { final StringSink _sink; _JsonStringStringifier( this._sink, dynamic Function(dynamic object)? _toEncodable, ) : super(_toEncodable); /// Convert object to a string. /// /// The [toEncodable] function is used to convert non-encodable objects /// to encodable ones. /// /// If [indent] is not `null`, the resulting JSON will be "pretty-printed" /// with newlines and indentation. The `indent` string is added as indentation /// for each indentation level. It should only contain valid JSON whitespace /// characters (space, tab, carriage return or line feed). static String stringify( Object? object, dynamic toEncodable(dynamic object)?, String? indent, ) { var output = StringBuffer(); printOn(object, output, toEncodable, indent); return output.toString(); } /// Convert object to a string, and write the result to the [output] sink. /// /// The result is written piecemally to the sink. static void printOn( Object? object, StringSink output, dynamic toEncodable(dynamic o)?, String? indent, ) { _JsonStringifier stringifier; if (indent == null) { stringifier = _JsonStringStringifier(output, toEncodable); } else { stringifier = _JsonStringStringifierPretty(output, toEncodable, indent); } stringifier.writeObject(object); } String? get _partialResult => _sink is StringBuffer ? _sink.toString() : null; void writeNumber(num number) { _sink.write(number.toString()); } void writeString(String string) { _sink.write(string); } void writeStringSlice(String string, int start, int end) { _sink.write(string.substring(start, end)); } void writeCharCode(int charCode) { _sink.writeCharCode(charCode); } } class _JsonStringStringifierPretty extends _JsonStringStringifier with _JsonPrettyPrintMixin { final String _indent; _JsonStringStringifierPretty( StringSink sink, dynamic toEncodable(dynamic o)?, this._indent, ) : super(sink, toEncodable); void writeIndentation(int count) { for (var i = 0; i < count; i++) writeString(_indent); } } /// Specialization of [_JsonStringifier] that writes the JSON as UTF-8. /// /// The JSON text is UTF-8 encoded and written to [Uint8List] buffers. /// The buffers are then passed back to a user provided callback method. class _JsonUtf8Stringifier extends _JsonStringifier { final int bufferSize; final void Function(Uint8List list, int start, int end) addChunk; Uint8List buffer; int index = 0; _JsonUtf8Stringifier( dynamic toEncodable(dynamic o)?, this.bufferSize, this.addChunk, ) : buffer = Uint8List(bufferSize), super(toEncodable); /// Convert [object] to UTF-8 encoded JSON. /// /// Calls [addChunk] with slices of UTF-8 code units. /// These will typically have size [bufferSize], but may be shorter. /// The buffers are not reused, so the [addChunk] call may keep and reuse the /// chunks. /// /// If [indent] is non-`null`, the result will be "pretty-printed" with extra /// newlines and indentation, using [indent] as the indentation. static void stringify( Object? object, List? indent, dynamic toEncodable(dynamic o)?, int bufferSize, void addChunk(Uint8List chunk, int start, int end), ) { _JsonUtf8Stringifier stringifier; if (indent != null) { stringifier = _JsonUtf8StringifierPretty( toEncodable, indent, bufferSize, addChunk, ); } else { stringifier = _JsonUtf8Stringifier(toEncodable, bufferSize, addChunk); } stringifier.writeObject(object); stringifier.flush(); } /// Must be called at the end to push the last chunk to the [addChunk] /// callback. void flush() { if (index > 0) { addChunk(buffer, 0, index); } buffer = Uint8List(0); index = 0; } String? get _partialResult => null; void writeNumber(num number) { writeAsciiString(number.toString()); } /// Write a string that is known to not have non-ASCII characters. void writeAsciiString(String string) { // TODO(lrn): Optimize by copying directly into buffer instead of going // through writeCharCode; for (var i = 0; i < string.length; i++) { var char = string.codeUnitAt(i); assert(char <= 0x7f); writeByte(char); } } void writeString(String string) { writeStringSlice(string, 0, string.length); } void writeStringSlice(String string, int start, int end) { // TODO(lrn): Optimize by copying directly into buffer instead of going // through writeCharCode/writeByte. Assumption is the most characters // in strings are plain ASCII. for (var i = start; i < end; i++) { var char = string.codeUnitAt(i); if (char <= 0x7f) { writeByte(char); } else { if ((char & 0xF800) == 0xD800) { // Surrogate. if (char < 0xDC00 && i + 1 < end) { // Lead surrogate. var nextChar = string.codeUnitAt(i + 1); if ((nextChar & 0xFC00) == 0xDC00) { // Tail surrogate. char = 0x10000 + ((char & 0x3ff) << 10) + (nextChar & 0x3ff); writeFourByteCharCode(char); i++; continue; } } // Unpaired surrogate. writeMultiByteCharCode(unicodeReplacementCharacterRune); continue; } writeMultiByteCharCode(char); } } } void writeCharCode(int charCode) { if (charCode <= 0x7f) { writeByte(charCode); return; } writeMultiByteCharCode(charCode); } void writeMultiByteCharCode(int charCode) { if (charCode <= 0x7ff) { writeByte(0xC0 | (charCode >> 6)); writeByte(0x80 | (charCode & 0x3f)); return; } if (charCode <= 0xffff) { writeByte(0xE0 | (charCode >> 12)); writeByte(0x80 | ((charCode >> 6) & 0x3f)); writeByte(0x80 | (charCode & 0x3f)); return; } writeFourByteCharCode(charCode); } void writeFourByteCharCode(int charCode) { assert(charCode <= 0x10ffff); writeByte(0xF0 | (charCode >> 18)); writeByte(0x80 | ((charCode >> 12) & 0x3f)); writeByte(0x80 | ((charCode >> 6) & 0x3f)); writeByte(0x80 | (charCode & 0x3f)); } void writeByte(int byte) { assert(byte <= 0xff); if (index == buffer.length) { addChunk(buffer, 0, index); buffer = Uint8List(bufferSize); index = 0; } buffer[index++] = byte; } } /// Pretty-printing version of [_JsonUtf8Stringifier]. class _JsonUtf8StringifierPretty extends _JsonUtf8Stringifier with _JsonPrettyPrintMixin { final List indent; _JsonUtf8StringifierPretty( dynamic toEncodable(dynamic o)?, this.indent, int bufferSize, void addChunk(Uint8List buffer, int start, int end), ) : super(toEncodable, bufferSize, addChunk); void writeIndentation(int count) { var indent = this.indent; var indentLength = indent.length; if (indentLength == 1) { var char = indent[0]; while (count > 0) { writeByte(char); count -= 1; } return; } while (count > 0) { count--; var end = index + indentLength; if (end <= buffer.length) { buffer.setRange(index, end, indent); index = end; } else { for (var i = 0; i < indentLength; i++) { writeByte(indent[i]); } } } } }