--- url: /reference/api.md --- # API Overview All types are in the `Cocoar.Json.Mutable` namespace. The library targets .NET 8.0. ## Thread Safety This library is **not thread-safe**. No type is safe for concurrent reads and writes from multiple threads. If you need to share a `MutableJsonObject` across threads, use external synchronization (e.g., a lock). After serialization, the resulting `byte[]` is an independent copy and can be safely shared across threads. ## MutableJsonNode Abstract base class for all JSON node types. | Member | Type | Description | |---|---|---| | `Kind` | `JsonValueKind` | The JSON value kind (Object, Array, String, Number, True, False, Null) | | `WriteTo(Utf8JsonWriter)` | `void` | Serialize this node to a writer | | `ToDebugString()` | `string` | Human-readable debug representation | ## MutableJsonObject Sealed. A JSON object with ordered, named properties. ### Constructor ```csharp MutableJsonObject(int indexThreshold = -1) ``` Creates an empty object. When `indexThreshold` is -1, uses `DefaultIndexThreshold` (default: 12). ### Static Properties | Member | Type | Description | |---|---|---| | `DefaultIndexThreshold` | `int` | Property count at which a dictionary index is built. Default: 12 | ### Properties | Member | Type | Description | |---|---|---| | `Properties` | `IReadOnlyList` | All properties in insertion order | | `Kind` | `JsonValueKind` | Always `JsonValueKind.Object` | ### Methods | Method | Return | Description | |---|---|---| | `Get(ReadOnlySpan)` | `MutableJsonNode?` | Get by UTF-8 property name | | `Get(string)` | `MutableJsonNode?` | Get by string property name | | `Set(ReadOnlySpan, MutableJsonNode)` | `void` | Set or overwrite by UTF-8 name. Overwrites preserve position | | `Set(string, MutableJsonNode)` | `void` | Set or overwrite by string name. Overwrites preserve position | | `Remove(ReadOnlySpan)` | `bool` | Remove by UTF-8 name. Returns `true` if found | | `Remove(string)` | `bool` | Remove by string name. Returns `true` if found | ### Property Struct `MutableJsonObject.Property` is a `readonly struct`: | Member | Type | Description | |---|---|---| | `NameUtf8` | `ReadOnlyMemory` | Property name as UTF-8 bytes | | `Name` | `string` | Property name as string (decoded on access) | | `Value` | `MutableJsonNode` | The property value | ## MutableJsonArray Sealed. A JSON array of nodes. Intentionally minimal — supports append and read. For removals or reordering, build a new array. ### Properties | Member | Type | Description | |---|---|---| | `Items` | `IReadOnlyList` | All items in order | | `Kind` | `JsonValueKind` | Always `JsonValueKind.Array` | ### Methods | Method | Return | Description | |---|---|---| | `Add(MutableJsonNode)` | `void` | Append an item | | `this[int]` | `MutableJsonNode` | Read-only indexer | ## MutableJsonString Sealed. A JSON string stored as UTF-8 bytes. ### Constructors | Constructor | Copies? | Description | |---|---|---| | `MutableJsonString(byte[])` | No | Stores the array reference directly — caller retains access to the internal buffer | | `MutableJsonString(string)` | Yes | Encodes to a new UTF-8 byte array | ### Static Factory Methods | Method | Description | |---|---| | `FromOwned(byte[])` | Takes ownership of the array — no copy | | `FromCopy(ReadOnlySpan)` | Copies the bytes into a new array | ### Properties & Methods | Member | Type | Description | |---|---|---| | `ValueUtf8` | `ReadOnlySpan` | The raw UTF-8 content | | `Kind` | `JsonValueKind` | Always `JsonValueKind.String` | | `Replace(byte[])` | `void` | Replace the value in place | ## MutableJsonNumber Sealed. A JSON number stored as raw UTF-8 digits. ### Constructors | Constructor | Description | |---|---| | `MutableJsonNumber(ReadOnlySpan)` | From raw UTF-8 digit bytes | | `MutableJsonNumber(int)` | From int | | `MutableJsonNumber(long)` | From long | | `MutableJsonNumber(double)` | From double | ### Static Factory Methods | Method | Description | |---|---| | `FromOwned(byte[])` | Takes ownership — no copy | | `FromCopy(ReadOnlySpan)` | Copies the bytes | ### Properties | Member | Type | Description | |---|---|---| | `ValueUtf8` | `ReadOnlySpan` | The raw UTF-8 representation | | `Kind` | `JsonValueKind` | Always `JsonValueKind.Number` | ## MutableJsonBool Sealed. A JSON boolean. ### Constructor ```csharp MutableJsonBool(bool value) ``` ### Properties | Member | Type | Description | |---|---|---| | `Kind` | `JsonValueKind` | `JsonValueKind.True` or `JsonValueKind.False` | ## MutableJsonNull Sealed. A JSON null. Singleton pattern. | Member | Type | Description | |---|---|---| | `Instance` | `MutableJsonNull` | The single null instance | | `Kind` | `JsonValueKind` | Always `JsonValueKind.Null` | ## MutableJsonDocument Static class. Entry point for parsing and serialization. ### Parsing | Method | Return | Description | |---|---|---| | `Parse(byte[])` | `MutableJsonNode` | Parse from byte array | | `Parse(ReadOnlySpan)` | `MutableJsonNode` | Parse from span | | `Parse(ReadOnlyMemory)` | `MutableJsonNode` | Parse from memory (provider-friendly) | | `ParseFromStream(Stream)` | `MutableJsonNode` | Parse from stream (64 KB initial buffer, grows exponentially) | All `Parse` methods throw `JsonException` for malformed input. ### Serialization | Method | Return | Description | |---|---|---| | `ToUtf8Bytes(MutableJsonNode)` | `byte[]` | Serialize to compact UTF-8 JSON | | `WriteTo(MutableJsonNode, Stream)` | `void` | Write to stream (compact) | | `WriteTo(MutableJsonNode, Stream, JsonWriterOptions)` | `void` | Write to stream with options | | `WriteToAsync(MutableJsonNode, Stream, CancellationToken)` | `Task` | Async write to stream | | `WriteToAsync(MutableJsonNode, Stream, JsonWriterOptions, CancellationToken)` | `Task` | Async write with options | ## MutableJsonMerge Static class. Merging and cloning operations. | Method | Return | Description | |---|---|---| | `Merge(MutableJsonObject, MutableJsonObject)` | `MutableJsonObject` | Non-destructive merge — clones source values | | `MergeDestructive(MutableJsonObject, MutableJsonObject)` | `MutableJsonObject` | Destructive merge — moves source values | | `Clone(MutableJsonNode)` | `MutableJsonNode` | Deep clone any node | Both merge methods return the target object for chaining. --- --- url: /guide/getting-started.md --- # Getting Started ## Install ```shell dotnet add package Cocoar.Json.Mutable ``` No additional dependencies — the library is built on `System.Text.Json` which ships with .NET. ## Parse and Merge Two JSON Objects The most common use case: merge multiple JSON documents into one. ```csharp using Cocoar.Json.Mutable; var baseConfig = MutableJsonDocument.Parse(""" { "server": { "port": 8080, "host": "0.0.0.0" }, "debug": false } """u8); var overrides = MutableJsonDocument.Parse(""" { "server": { "host": "localhost" }, "debug": true } """u8); var result = new MutableJsonObject(); MutableJsonMerge.Merge(result, (MutableJsonObject)baseConfig); MutableJsonMerge.Merge(result, (MutableJsonObject)overrides); // Result: { "server": { "port": 8080, "host": "localhost" }, "debug": true } byte[] json = MutableJsonDocument.ToUtf8Bytes(result); ``` Properties merge recursively. The second call to `Merge` overwrites `host` and `debug` but leaves `port` intact. ## Build a JSON Object from Scratch You don't have to start from parsed JSON. Build structures programmatically using the string API: ```csharp var doc = new MutableJsonObject(); doc.Set("name", new MutableJsonString("MyApp")); doc.Set("version", new MutableJsonNumber(2)); doc.Set("enabled", new MutableJsonBool(true)); var tags = new MutableJsonArray(); tags.Add(new MutableJsonString("production")); tags.Add(new MutableJsonString("v2")); doc.Set("tags", tags); byte[] json = MutableJsonDocument.ToUtf8Bytes(doc); // {"name":"MyApp","version":2,"enabled":true,"tags":["production","v2"]} ``` ## Read Values Back Access properties by name and cast to the expected node type: ```csharp var config = (MutableJsonObject)MutableJsonDocument.Parse(""" { "server": { "port": 8080 }, "name": "MyApp" } """u8); // String API var server = config.Get("server") as MutableJsonObject; var port = server?.Get("port") as MutableJsonNumber; // UTF-8 API (zero allocation) var name = config.Get("name"u8) as MutableJsonString; ``` Both APIs can be mixed freely within the same codebase. ## Next Steps * [Why Cocoar.Json.Mutable?](/guide/why) — How it compares to raw System.Text.Json * [Node Types](/guide/node-types) — All available node types and their API * [Parsing & Serialization](/guide/parsing-serialization) — Reading and writing JSON * [Merging](/guide/merging) — Deep merge strategies and cloning --- --- url: /guide/merging.md --- # Merging Merging is the core operation of `Cocoar.Json.Mutable`. The `MutableJsonMerge` class provides two merge strategies and a deep clone. ## Non-Destructive Merge `Merge` copies values from the source into the target. The source remains unchanged: ```csharp var target = new MutableJsonObject(); var source = (MutableJsonObject)MutableJsonDocument.Parse(""" { "server": { "port": 8080 } } """u8); MutableJsonMerge.Merge(target, source); // target now has { "server": { "port": 8080 } } // source is unchanged — its values were cloned ``` Use this when you need the source object after merging (e.g., to merge it into multiple targets). ## Destructive Merge `MergeDestructive` moves values from the source into the target. The source should not be used after the call: ```csharp var target = new MutableJsonObject(); var source = (MutableJsonObject)MutableJsonDocument.Parse(""" { "server": { "port": 8080 } } """u8); MutableJsonMerge.MergeDestructive(target, source); // target now has { "server": { "port": 8080 } } // source still has its property list, but values are shared with target — do not use source ``` Destructive merge is faster because it avoids cloning. Use it when you parse a document only to merge it and discard the source. ## Merge Behavior Both merge strategies follow the same rules: | Source Value | Target Has Key? | Existing Target Value | Result | |---|---|---|---| | Object | Yes | Object | Recursive merge into existing object | | Object | Yes | Non-object | Source replaces target value | | Object | No | — | Source added to target | | Non-object | Yes | Any | Source replaces target value | | Non-object | No | — | Source added to target | The key distinction: when both source and target have the same property and both are objects, the merge recurses into the nested object rather than replacing it. **All other types — including arrays — are replaced entirely.** ### Arrays Are Replaced, Not Merged When both source and target have an array under the same key, the source array replaces the target array. Items are not appended or merged by index: ```csharp var target = (MutableJsonObject)MutableJsonDocument.Parse(""" { "tags": ["a", "b", "c"] } """u8); var source = (MutableJsonObject)MutableJsonDocument.Parse(""" { "tags": ["x"] } """u8); MutableJsonMerge.Merge(target, source); // Result: { "tags": ["x"] } // The entire array was replaced — "a", "b", "c" are gone ``` This is deliberate. Array merging is inherently ambiguous (append? by-index? deduplicate?), so the library makes the simplest, most predictable choice: full replacement. If you need custom array merging logic, do it before calling `Merge`. ### Example: Recursive Object Merge ```csharp var target = (MutableJsonObject)MutableJsonDocument.Parse(""" { "server": { "port": 8080, "host": "0.0.0.0" }, "debug": false } """u8); var source = (MutableJsonObject)MutableJsonDocument.Parse(""" { "server": { "host": "localhost" }, "version": "2.0" } """u8); MutableJsonMerge.Merge(target, source); // Result: // { // "server": { "port": 8080, "host": "localhost" }, // "debug": false, // "version": "2.0" // } ``` * `server.host` was overwritten (`"localhost"` replaces `"0.0.0.0"`) * `server.port` was preserved (source didn't set it) * `debug` was preserved (source didn't set it) * `version` was added (new property) ## Multi-Source Merging Merge multiple sources in sequence — last write wins: ```csharp var result = new MutableJsonObject(); var sources = new[] { """{ "log": "info", "port": 3000 }""", """{ "port": 8080 }""", """{ "log": "debug", "host": "localhost" }""", }; foreach (var json in sources) { var source = (MutableJsonObject)MutableJsonDocument.Parse( System.Text.Encoding.UTF8.GetBytes(json)); MutableJsonMerge.MergeDestructive(result, source); } // Result: { "log": "debug", "port": 8080, "host": "localhost" } ``` This pattern is ideal for layered configuration: base settings, environment overrides, user preferences. ## Deep Clone Clone any node to create a fully independent copy: ```csharp var original = (MutableJsonObject)MutableJsonDocument.Parse(""" { "items": [1, 2, 3], "name": "test" } """u8); var copy = (MutableJsonObject)MutableJsonMerge.Clone(original); // Modify the copy — original is unaffected copy.Set("name", new MutableJsonString("modified")); ``` Clone works recursively on all node types: objects, arrays, strings, numbers, booleans, and null. ## Choosing a Strategy | Scenario | Strategy | Why | |---|---|---| | Merge parsed config, discard source | `MergeDestructive` | Faster — no cloning overhead | | Merge source into multiple targets | `Merge` | Source stays valid for reuse | | Keep a backup before merging | `Clone` + `MergeDestructive` | Clone the target first, then merge fast | --- --- url: /guide/node-types.md --- # Node Types All JSON values are represented as subclasses of `MutableJsonNode`. The type hierarchy mirrors the JSON spec: objects, arrays, strings, numbers, booleans, and null. ## MutableJsonNode The abstract base class. Every node exposes: ```csharp public abstract JsonValueKind Kind { get; } public abstract void WriteTo(Utf8JsonWriter writer); ``` `Kind` returns the `System.Text.Json.JsonValueKind` for the node — useful for type checks without casting. ## MutableJsonObject A JSON object with named properties. Properties are stored in insertion order. When `Set` overwrites an existing property, the value is replaced at its original position — the property order does not change. ```csharp var obj = new MutableJsonObject(); // Set properties obj.Set("name", new MutableJsonString("MyApp")); obj.Set("port", new MutableJsonNumber(8080)); // Get properties (returns null if not found) var name = obj.Get("name") as MutableJsonString; // Remove properties obj.Remove("port"); // Iterate all properties foreach (var prop in obj.Properties) { Console.WriteLine($"{prop.Name} = {prop.Value.Kind}"); } ``` ### UTF-8 API Every `Get`, `Set`, and `Remove` method has a `ReadOnlySpan` overload: ```csharp obj.Set("host"u8, new MutableJsonString("localhost"u8)); var host = obj.Get("host"u8); obj.Remove("host"u8); ``` ### Automatic Indexing For objects with few properties, lookups scan the property list linearly. When the property count reaches a threshold (default: 12), a dictionary index is built automatically. You can control this: ```csharp // Custom threshold for this object var largeObj = new MutableJsonObject(indexThreshold: 5); // Change the default for all new objects MutableJsonObject.DefaultIndexThreshold = 20; ``` ### Property Struct Each property is a `readonly struct` with: | Member | Type | Description | |---|---|---| | `NameUtf8` | `ReadOnlyMemory` | Property name as UTF-8 bytes | | `Name` | `string` | Property name decoded to string | | `Value` | `MutableJsonNode` | The property value | ## MutableJsonArray A JSON array of nodes. The API is intentionally minimal — `Add` and read-only indexing. There are no `RemoveAt`, `Insert`, or `Clear` methods. If you need a different set of items, build a new array. This matches the library's focus: merge operations replace arrays wholesale (see [Merging](/guide/merging)), so fine-grained array manipulation is rarely needed. ```csharp var arr = new MutableJsonArray(); arr.Add(new MutableJsonString("first")); arr.Add(new MutableJsonNumber(42)); arr.Add(MutableJsonNull.Instance); // Access by index var first = arr[0] as MutableJsonString; // Count int count = arr.Items.Count; // Iterate foreach (var item in arr.Items) { Console.WriteLine(item.Kind); } ``` ## MutableJsonString A JSON string stored as UTF-8 bytes. ```csharp // From a .NET string (encodes to a new byte[] internally) var str = new MutableJsonString("hello"); // From a byte[] (stores the reference directly — no copy) byte[] raw = "hello"u8.ToArray(); var str2 = new MutableJsonString(raw); // str2 and raw share the same array // Access the raw bytes ReadOnlySpan utf8 = str.ValueUtf8; // Replace the value in place str.Replace("world"u8.ToArray()); ``` ### Constructors and Factory Methods | Method | Copies? | Description | |---|---|---| | `MutableJsonString(byte[])` | No | Stores the array reference directly | | `MutableJsonString(string)` | Yes (encodes) | Encodes the string to a new UTF-8 byte array | | `FromOwned(byte[])` | No | Same as the byte\[] constructor — explicit ownership intent | | `FromCopy(ReadOnlySpan)` | Yes | Copies the data into a new array | Use the `byte[]` constructor or `FromOwned` when you have a byte array you own and want to keep a reference to (e.g., for zeroing later). Use `FromCopy` when the source data might change or be reused. ## MutableJsonNumber A JSON number stored as raw UTF-8 digits. The number is never parsed to `int` or `double` — it stays in its original representation until serialized. ```csharp // From .NET numeric types var fromInt = new MutableJsonNumber(42); var fromLong = new MutableJsonNumber(9_999_999_999L); var fromDouble = new MutableJsonNumber(3.14); // From raw UTF-8 bytes var fromBytes = new MutableJsonNumber("42"u8); // Access the raw representation ReadOnlySpan raw = fromInt.ValueUtf8; // "42" as UTF-8 bytes ``` ### Factory Methods | Method | Behavior | |---|---| | `FromOwned(byte[])` | Takes ownership — no copy | | `FromCopy(ReadOnlySpan)` | Copies the data | ## MutableJsonBool A JSON boolean. ```csharp var t = new MutableJsonBool(true); var f = new MutableJsonBool(false); // Kind tells you the value bool isTrue = t.Kind == JsonValueKind.True; ``` ## MutableJsonNull A JSON null. Singleton — there's only one instance: ```csharp var n = MutableJsonNull.Instance; // n.Kind == JsonValueKind.Null ``` Use `MutableJsonNull.Instance` wherever you need a null value. The private constructor ensures no unnecessary allocations. --- --- url: /guide/parsing-serialization.md --- # Parsing & Serialization All parsing and serialization goes through the `MutableJsonDocument` static class. It wraps `System.Text.Json`'s `Utf8JsonReader` and `Utf8JsonWriter` internally. ## Parsing ### From UTF-8 Bytes The primary parse path. Works with byte arrays, spans, and memory: ```csharp // From a UTF-8 literal (ReadOnlySpan) var node = MutableJsonDocument.Parse("{\"port\": 8080}"u8); // From a byte array byte[] data = File.ReadAllBytes("config.json"); var node2 = MutableJsonDocument.Parse(data); // From ReadOnlyMemory — ideal for provider scenarios ReadOnlyMemory memory = GetDataFromProvider(); var node3 = MutableJsonDocument.Parse(memory); ``` All three overloads return a `MutableJsonNode`. For JSON objects (the most common case), cast the result: ```csharp var obj = (MutableJsonObject)MutableJsonDocument.Parse(jsonBytes); ``` All `Parse` methods throw `JsonException` for malformed input — the same exception type and error messages as `System.Text.Json`. There is no `TryParse` variant. ### From a Stream For large files or network streams, use `ParseFromStream` to avoid loading the entire payload into memory at once: ```csharp using var stream = File.OpenRead("large-config.json"); var node = MutableJsonDocument.ParseFromStream(stream); ``` The parser uses `ArrayPool` internally with a 64 KB initial buffer and grows it exponentially as needed. The buffer is returned to the pool after parsing — individual node values are copied into their own dedicated `byte[]` arrays. ## Serialization ### To Byte Array ```csharp byte[] utf8Json = MutableJsonDocument.ToUtf8Bytes(node); ``` Returns compact (unindented) JSON as a UTF-8 byte array. ### To Stream Write directly to a stream without intermediate allocation: ```csharp // Default options (compact) using var stream = File.Create("output.json"); MutableJsonDocument.WriteTo(node, stream); // With custom options (e.g. indented) var options = new JsonWriterOptions { Indented = true }; MutableJsonDocument.WriteTo(node, stream, options); ``` ### Async Writing For non-blocking I/O: ```csharp await using var stream = File.Create("output.json"); await MutableJsonDocument.WriteToAsync(node, stream); // With options var options = new JsonWriterOptions { Indented = true }; await MutableJsonDocument.WriteToAsync(node, stream, options); ``` ## Round-Trip Example Parse, modify, serialize: ```csharp // Parse var config = (MutableJsonObject)MutableJsonDocument.Parse(""" { "server": { "port": 8080 }, "debug": false } """u8); // Modify config.Set("debug", new MutableJsonBool(true)); var server = config.Get("server") as MutableJsonObject; server?.Set("host", new MutableJsonString("localhost")); // Serialize byte[] result = MutableJsonDocument.ToUtf8Bytes(config); // {"server":{"port":8080,"host":"localhost"},"debug":true} ``` --- --- url: /guide/utf8-api.md --- # UTF-8 API & Memory Management This library stores all string and number data as UTF-8 `byte[]`. This page explains the memory model and how to choose between the string API and the UTF-8 API. ## Why UTF-8? JSON is typically transmitted and stored as UTF-8. Most .NET JSON libraries decode these bytes into .NET `string` (UTF-16) on parse, then re-encode to UTF-8 on serialization. This double conversion is unnecessary when the goal is merging — the merged result goes back to UTF-8 anyway. `Cocoar.Json.Mutable` skips the conversion entirely. Property names and string values stay as `byte[]` from parse to serialize. ## The Two APIs ### String API Convenient for application code where allocations aren't a concern: ```csharp obj.Set("name", new MutableJsonString("MyApp")); var value = obj.Get("name"); obj.Remove("name"); ``` Each call to `Set(string, ...)` or `Get(string)` encodes the key to UTF-8 internally. This is fine for most code — the encoding is fast and the allocation is small. ### UTF-8 API For hot paths where you want zero allocations on the key lookup: ```csharp obj.Set("name"u8, new MutableJsonString("MyApp"u8)); var value = obj.Get("name"u8); obj.Remove("name"u8); ``` The `u8` suffix creates a `ReadOnlySpan` at compile time. No runtime encoding or allocation. ## Memory Ownership ### Constructors `MutableJsonString` and `MutableJsonNumber` offer multiple ways to provide data: ```csharp // Constructor (byte[]): stores the reference directly — no copy byte[] buffer = GetBuffer(); var s1 = new MutableJsonString(buffer); // s1 and buffer share the same array // Constructor (string): encodes to a new byte[] — the caller has no reference to it var s2 = new MutableJsonString("hello"); // FromOwned: same as the byte[] constructor — takes ownership, no copy var s3 = MutableJsonString.FromOwned(buffer); // FromCopy: copies the span into a new array ReadOnlySpan span = GetSpan(); var s4 = MutableJsonString.FromCopy(span); ``` | Method | Copies? | Caller holds reference to internal array? | Use When | |---|---|---|---| | Constructor (`byte[]`) | No | Yes | You own the array and want to keep a reference (e.g., for zeroing) | | Constructor (`string`) | Yes (encodes) | No | You have a .NET string | | `FromOwned(byte[])` | No | Yes | Same as constructor — explicit intent | | `FromCopy(ReadOnlySpan)` | Yes | No | The source buffer might change or be reused | ### Reading Values `ValueUtf8` returns a `ReadOnlySpan` — a view into the internal array without copying: ```csharp var str = new MutableJsonString("hello"); ReadOnlySpan bytes = str.ValueUtf8; // No allocation ``` ::: warning The span is only valid while the node exists and hasn't been replaced. Don't store it across async boundaries — copy it to a `byte[]` if you need to keep it. ::: ## Parsing and Memory When `MutableJsonDocument.Parse` processes JSON, it copies relevant byte ranges from the input into new `byte[]` for each string and number value. The input buffer is not retained — you can free or reuse it after parsing. For stream parsing, `ParseFromStream` uses `ArrayPool` to rent a read buffer. The buffer is returned to the pool after parsing completes — individual node values are copied into their own arrays. ## When NOT to Use UTF-8 API The UTF-8 API is not always the right choice: * **Property names are dynamic** (user input, config keys) — use the string API to avoid manual encoding * **You need the value as a .NET string** — just use `new MutableJsonString(myString)` and let the library encode once * **Allocations don't matter** — if you're merging a few configs at startup, the string API is simpler and equally fast The UTF-8 API is most valuable in tight loops or high-throughput scenarios where you're merging many documents per second. --- --- url: /guide/why.md --- # Why Cocoar.Json.Mutable? ## The Problem with System.Text.Json .NET provides two built-in ways to work with JSON at the DOM level. Neither is designed for merging. ### JsonDocument — Immutable `System.Text.Json.JsonDocument` is fast for reading but immutable. You cannot change a property, add an element, or merge two documents without serializing and re-parsing: ```csharp // Reading is fine using var doc = JsonDocument.Parse(json); var port = doc.RootElement.GetProperty("server").GetProperty("port").GetInt32(); // But merging two documents? No built-in support. // You'd have to manually walk both trees and write to a Utf8JsonWriter. ``` ### JsonNode — Mutable but String-Based `System.Text.Json.Nodes.JsonNode` is mutable, but it stores all values as managed .NET objects and uses string-based property names: ```csharp var node = JsonNode.Parse(json); node["server"]["port"] = 9090; // Works, but every property name is a string allocation // Merging requires manual recursion + DeepClone for every value var clone = source.DeepClone(); // Allocates the entire subtree ``` Merging `JsonNode` trees is manual, allocation-heavy, and gets complex with nested objects. ## The Cocoar Approach `Cocoar.Json.Mutable` is purpose-built for the merge use case: ```csharp var target = new MutableJsonObject(); // Merge any number of sources — each call is one line MutableJsonMerge.Merge(target, (MutableJsonObject)MutableJsonDocument.Parse(baseJson)); MutableJsonMerge.Merge(target, (MutableJsonObject)MutableJsonDocument.Parse(overrideJson)); MutableJsonMerge.Merge(target, (MutableJsonObject)MutableJsonDocument.Parse(envJson)); byte[] result = MutableJsonDocument.ToUtf8Bytes(target); ``` ### Comparison | Capability | JsonDocument | JsonNode | Cocoar.Json.Mutable | |---|---|---|---| | Mutable | No | Yes | Yes | | Deep merge | Not supported | Manual recursion | `Merge()` / `MergeDestructive()` | | String storage | UTF-8 (readonly) | .NET strings | UTF-8 byte arrays | | Property access | Span-based | String-based | Both (string + UTF-8) | | Memory model | Pooled, disposable | GC-managed | GC-managed, no zeroing | | Deep clone | Not applicable | `DeepClone()` | `Clone()` | | Provider input | `ReadOnlyMemory` | String | `byte[]`, `ReadOnlySpan`, `ReadOnlyMemory` | ## Memory Ownership — Why It Matters for Secrets Beyond merging, there is a second reason this library exists: **byte array ownership**. When a configuration system handles secrets (API keys, connection strings, encryption keys), the secret data must be zeroable after use. .NET strings are immutable and managed by the garbage collector — once a secret lands in a `string`, it cannot be erased from memory. It stays until the GC collects it, and you have no control over when or whether the memory is actually overwritten. The built-in JSON APIs make this problem worse: | API | What happens to secret values | Can you zero them? | |---|---|---| | `JsonDocument` | Internal buffers are pooled via `ArrayPool`. Secret bytes go back into the pool and may be handed to unrelated code. | No — you don't own the buffers | | `JsonNode` | Values are converted to .NET `string`. | No — strings are immutable | | `Utf8JsonReader` | `ValueSpan` points into the input buffer. For escaped strings, the reader copies into an internal buffer you don't control. | Partially — you own the input buffer, but not the reader's internal copy | `Cocoar.Json.Mutable` solves this by design: ```csharp // Parser always calls .ToArray() — every value gets its own byte[] JsonTokenType.String => new MutableJsonString(reader.ValueSpan.ToArray()) ``` Every parsed value is copied into a dedicated `byte[]` that the caller fully owns. No shared pools, no .NET strings, no internal buffers. The caller who created or received the `byte[]` can zero it when done: ```csharp // Keep a reference to the byte array you own byte[] apiKeyBytes = GetDecryptedSecret(); var secret = MutableJsonString.FromOwned(apiKeyBytes); // Use the secret... ReadOnlySpan value = secret.ValueUtf8; // Zero the original array when done — this clears the node's internal data CryptographicOperations.ZeroMemory(apiKeyBytes); ``` ::: warning `ValueUtf8` returns a `ReadOnlySpan` — you cannot zero through it. You must hold a reference to the original `byte[]` that was passed to the constructor or `FromOwned`. Calling `.ToArray()` on the span creates a *new* array and zeroing that copy does nothing to the node's internal data. ::: This is exactly how [Cocoar.Configuration](https://github.com/cocoar-dev/Cocoar.Configuration) handles secrets: `Secret` keeps a reference to the owned byte arrays, provides lease-based access, and calls `Array.Clear()` on dispose — ensuring secret material never touches a .NET string and is wiped from memory as soon as the lease ends. ::: info The library itself does not perform automatic zeroing — it provides the **mechanism** (byte array ownership). The **policy** (when and how to zero) is implemented by the consuming code, such as `Secret` in Cocoar.Configuration. ::: ### Design Principles **Merge-first.** The core operation is combining multiple JSON sources into one object. Everything else — parsing, serialization, node types — supports that use case. **Byte ownership.** Every value is stored in a dedicated `byte[]` that the caller owns. No shared pools, no immutable strings. This enables downstream code to zero sensitive data after use. **UTF-8 throughout.** Property names and string values are stored as `byte[]`. No encoding conversion until you explicitly need a .NET string. Numbers are stored as raw UTF-8 digits — no parsing to `double` until you need it. **Dual API.** The string API (`Get("name")`, `Set("key", value)`) makes code readable. The UTF-8 API (`Get("name"u8)`) avoids allocations in hot paths. Both work on the same data.