Editing Trees
ToonDocument.Parse gives a tree of ToonValues (ToonObject, ToonArray, ToonString, ToonNumber,
ToonBoolean, ToonNull) that can be read, changed and written back, in the manner of System.Text.Json.Nodes.
Navigating
Every value has indexers, so lookups chain:
using ToonNet.Core.Models;
var root = ToonDocument.Parse(toon).Root;
string name = root["users"]![0]["name"]!.GetValue<string>();
int total = root["meta"]!["total"]!.GetValue<int>();
ToonValue? missing = root["nope"]; // null: no such member
value["key"]on an object returns the member or null;value[i]on an array returns the item. On a value of another kind they throwInvalidOperationException("The value is an array, not an object.").AsObject()andAsArray()return the value as aToonObjectorToonArray, or throw.GetValue<T>()converts a primitive to a string, boolean, number, date/time,Guid,Urior enum with the serializer's rules ("2025-03-04"toDateOnly,"Friday"toDayOfWeek); it throwsToonSerializationExceptionwhen the value does not convert.TryGetValue<T>(out value)returns false instead. AToonValuetype the value is (GetValue<ToonNumber>()) gives the value itself, andGetValue<object>()gives what reading the primitive intoobjectgives: a string, a boolean, along,decimalordouble, or null.Deserialize<T>(options)converts any value with the serializer (also with aToonTypeInfo):var users = root["users"]!.Deserialize<List<User>>();
Changing
root["meta"]!["total"] = 3; // set a member (null stores a TOON null)
root["users"]![0] = "gone"; // set an item
root["users"]!.AsArray().Add(new ToonObject { ["name"] = "Cy" });
root.AsObject().Properties.Remove("draft");
Paths set and remove values anywhere in the tree (see Querying for the syntax):
root.SetValue("$.meta.updatedBy", "selcuk"); // creates missing objects on the way
root.SetValue("$.audit.history[0].at", 1); // an index equal to the length appends
int removed = root.RemoveAll("$.users[?@.active == false]");
SetValue takes a path of member names and indexes; a missing member becomes an object, or an array when an index
follows it. It checks the whole path first, so a path that does not fit the tree throws InvalidOperationException
and leaves the tree unchanged; a path with wildcards or filters throws too. RemoveAll takes any path and removes what
it selects; a value selected twice ([0,0]) counts once, and RemoveAll("$") throws InvalidOperationException
("The root cannot be removed.").
Items of a ToonArray is a List<ToonValue>: a null that code adds to it is read as a TOON null everywhere
(the indexer, paths, the serializer, DeepClone, DeepEquals, encoding).
Copying and Comparing
var copy = root.DeepClone(); // changes to the copy do not reach root
bool same = ToonValue.DeepEquals(root, copy); // true
DeepEquals compares data: objects with the same members in any order, arrays item by item, numbers by value (1
equals 1.0). Primitives cannot change, so DeepClone shares them.
Merge Patch
ToonMergePatch applies a JSON Merge Patch (RFC 7386): a document that looks like the target, with the members to
change, null for the members to remove, and the rest left out.
var patch = ToonDocument.Parse("""
meta:
region: us
draft: null
""").Root;
root = ToonMergePatch.Apply(root, patch); // sets meta.region, removes meta.draft
Apply changes objects in place and returns the result (a new value when the patch is not an object); call
DeepClone first to keep the original. An object that is in the tree twice (the same instance added at two places)
changes at both. ToonMergePatch.Create(original, modified) gives the patch between two trees,
so a client can send only what it changed:
var patch = ToonMergePatch.Create(before, after);
string body = new ToonEncoder().Encode(new ToonDocument(patch));
A merge patch replaces arrays as a whole and cannot set a member to null (null removes it).
See Also
- Querying: JSONPath over trees and text
- Type System: how .NET types map to TOON values