JSON serialization in the Dart SDK is a sophisticated system that balances platform-native capabilities with cross-platform consistency. Below is an analysis of its inner workings, platform specializations, and a proposal for a streaming API.
The core of JSON serialization in dart:convert is built around a Visitor Pattern implemented in the internal _JsonStringifier class.
- Encoder (
JsonEncoder): Traverses the object graph. Primitives (num,String,bool,Null) and standard containers (List,Map) are handled directly. For other objects, it callstoEncodable(defaulting to.toJson()), which is expected to return a serializable object. - Specializations: The SDK provides
_JsonStringStringifier(forStringoutput) and_JsonUtf8Stringifier(for directList<int>output). The UTF-8 version is highly optimized to avoid intermediate string allocations by writing directly to byte buffers. - Decoder (
JsonDecoder): Uses anexternalmethod_parseJsonwhich is patched per-platform. It supports both full-string conversion and chunked conversion via sinks.
| Feature | Dart VM | JS (dart2js/ddc) | Wasm (dart2wasm) |
|---|---|---|---|
| Parsing Engine | Hand-written Dart state machine. | Native JSON.parse. |
Hand-written Dart state machine (optimized for Wasm). |
| Streaming | True Streaming: The state machine maintains partial state across chunks. | Fake Streaming: Buffers the entire input into a StringBuffer before parsing. |
True Streaming: Similar to the VM implementation. |
| Object Creation | Immediate creation of List and Map during parsing. |
Lazy Decoding: Returns a _JsonMap wrapper. Values are converted to Dart objects only when accessed. |
Immediate creation using optimized WasmArray structures. |
| UTF-8 Support | Direct decoding from bytes via _JsonUtf8Parser. |
Usually requires conversion to a String first. |
Direct decoding from bytes, leveraging JS string interning for keys. |
| Key Optimization | Linear scan for cycle detection; standard Map hashing. | Leverages JS engine's hidden classes/optimization for objects. | Uses a specialized string interning cache for JSON keys to save memory and time. |
-
True JS Streaming: The current JS
JsonDecoderbuffers the entire input. For multi-gigabyte JSON files, this leads toOutOfMemoryerrors. A Dart-implemented streaming parser for JS could alleviate this, even if it's slower thanJSON.parse. -
Allocation Reduction: In
toJson()patterns, a newMapis created just to be immediately iterated and discarded. This is a massive source of short-lived garbage. -
Cycle Detection: The current
_seenlist uses a linear search (identical). For extremely deep graphs, this becomes$O(N^2)$ . While rare in JSON, a more efficient mechanism could be used for larger depths. - Wasm String Interop: Further reducing the boundary crossing between Wasm memory and JS strings during parsing.
The current "toJson" contract is a bottleneck: Object -> Map -> String. A streaming API would enable Object -> String directly.
We could introduce a public JsonWriter interface that mirrors the internal _JsonStringifier:
abstract interface class JsonWriter {
void writeString(String value);
void writeNumber(num value);
void writeBool(bool value);
void writeNull();
void startObject();
void endObject();
void writePropertyName(String name);
void startArray();
void endArray();
/// Write any serializable object using standard logic
void writeValue(Object? value);
}And a companion interface for classes:
abstract interface class JsonStreamEncodable {
void writeJson(JsonWriter writer);
}- Modified Encoder:
JsonEncoderwould check if an object implementsJsonStreamEncodable. - No Intermediate Maps: Instead of calling
toJson(), the encoder passes theJsonWritertowriteJson. - Seamless Integration: A
Userobject could write its fields directly to the underlyingStringConversionSinkorByteConversionSink.
Example Usage:
class User implements JsonStreamEncodable {
final String name;
final List<User> friends;
@override
void writeJson(JsonWriter writer) {
writer.startObject();
writer.writePropertyName('name');
writer.writeString(name);
writer.writePropertyName('friends');
writer.startArray();
for (var friend in friends) {
friend.writeJson(writer); // Recursive streaming!
}
writer.endArray();
writer.endObject();
}
}- Zero-Garbage Serialization: No intermediate
MaporListobjects for custom types. - Performance: Drastically reduces pressure on the GC, especially in high-throughput servers or memory-constrained environments (Wasm/Mobile).
- Symmetry: On the decoding side, exposing a
JsonListenerwould allow similar "SAX-style" parsing, enabling the processing of massive datasets that don't fit in memory.