| name | system-text-json-dom-and-streaming |
| version | 10.0.0 |
| description | Use when System.Text.Json work is not a plain POCO round-trip — inspecting/mutating JSON without a model (including answering a few questions about an unknown payload), editing JSON whose shape is not controlled by the app, reading or writing at high throughput (Utf8JsonReader / Utf8JsonWriter, byte and Stream overloads), or processing a JSON array/feed too large to fit in memory (DeserializeAsyncEnumerable). |
DOM & streaming APIs
Required setup
The DOM types are not in the System.Text.Json namespace. JsonNode/JsonObject/JsonArray
live in System.Text.Json.Nodes; forgetting that using is the usual reason this code won't compile
(JsonDocument, JsonElement, Utf8JsonReader and Utf8JsonWriter are in System.Text.Json):
using System.Text.Json;
using System.Text.Json.Nodes;
Build one options instance and reuse it — a fresh one is expensive (it caches per-type metadata on
first use). options in the examples below is that instance:
static readonly JsonSerializerOptions Options = new(JsonSerializerDefaults.Web);
Mutable DOM — JsonNode (the JObject replacement)
Use when you need to read/modify JSON without a CLR type.
JsonNode root = JsonNode.Parse(json)!;
string name = (string)root["user"]!["name"]!;
root["user"]!["active"] = true;
root["tags"]!.AsArray().Add("new");
string outJson = root.ToJsonString(new JsonSerializerOptions { WriteIndented = true });
JsonNode (mutable) → JsonObject / JsonArray / JsonValue. This is the Newtonsoft JObject/
JArray/JToken analog.
node.GetValue<T>() or explicit casts pull typed values; missing paths return null (guard with !).
Read-only DOM — JsonDocument / JsonElement
Fastest way to inspect JSON you won't mutate. JsonDocument is IDisposable (it rents pooled
buffers) — dispose it, and don't let a JsonElement outlive its document:
using JsonDocument doc = JsonDocument.Parse(json);
JsonElement root = doc.RootElement;
foreach (JsonElement item in root.GetProperty("items").EnumerateArray())
Console.WriteLine(item.GetProperty("id").GetInt32());
Low-level Utf8JsonReader / Utf8JsonWriter (hottest paths)
Allocation-free forward-only read/write over UTF-8 bytes — reach for these only when profiling
demands it (custom converters use them internally):
var writer = new Utf8JsonWriter(bufferWriter);
writer.WriteStartObject();
writer.WriteString("name", "Ada");
writer.WriteNumber("count", 3);
writer.WriteEndObject();
writer.Flush();
var reader = new Utf8JsonReader(utf8Bytes);
while (reader.Read()) { }
Bytes & streams — skip the intermediate string
For I/O, prefer the UTF-8 / Stream overloads over Serialize→string→Encoding.UTF8.GetBytes:
byte[] utf8 = JsonSerializer.SerializeToUtf8Bytes(value, options);
await JsonSerializer.SerializeAsync(stream, value, options);
var value = await JsonSerializer.DeserializeAsync<MyType>(stream, options);
Streaming large arrays — DeserializeAsyncEnumerable
Process a huge top-level JSON array without buffering it all in memory:
await foreach (var item in JsonSerializer.DeserializeAsyncEnumerable<Item>(stream, options))
Handle(item);
- Only the current item is materialized — ideal for large feeds/logs.
- All of the above compose with source generation: pass a
JsonTypeInfo<T> from a generated context
instead of options to stay AOT-safe. Source generation is covered separately.