Creating and Mutating Objects
This recipe demonstrates the full mutable document lifecycle using the V5 API: parsing an immutable document, creating a mutable builder, reading and modifying properties, mutating nested arrays, and serializing the result.
The Pattern
The V5 API separates immutable and mutable representations of JSON documents. You start with an immutable ParsedJsonDocument<T>, create a mutable JsonDocumentBuilder<T.Mutable> via a JsonWorkspace, and then modify properties, add or remove array items, and serialize the result. The workspace manages pooled memory so that mutations are efficient and allocation-free in the hot path.
Sub-element references (e.g. a mutable array obtained from a property) track the document version and become stale after mutations. Always re-fetch sub-elements from the root after modifying the document.
The Schema
File: person.json
{
"title": "Person",
"type": "object",
"required": ["name", "age"],
"properties": {
"name": {
"type": "object",
"required": ["familyName"],
"properties": {
"familyName": { "type": "string" },
"givenName": { "type": "string" }
}
},
"age": { "type": "integer", "format": "int32" },
"email": { "type": "string", "format": "email" },
"hobbies": {
"type": "array",
"items": { "type": "string" }
}
}
}
The generated .NET properties are:
Name— of typePerson.NameEntity(a nested object withFamilyNameandGivenName)Age— of typePerson.AgeEntity(an int32 type)Email— of typePerson.EmailEntity(an email-formatted string type)Hobbies— of typePerson.HobbiesEntity(an array of strings)
Generated Code Usage
Parsing an immutable document
string personJson =
"""
{
"name": {
"familyName": "Smith",
"givenName": "John"
},
"age": 30,
"email": "john@example.com",
"hobbies": ["reading", "hiking"]
}
""";
using var parsedDoc = ParsedJsonDocument<Person>.Parse(personJson);
Person person = parsedDoc.RootElement;
Creating a mutable builder
using JsonWorkspace workspace = JsonWorkspace.Create();
using var builder = person.CreateBuilder(workspace);
Person.Mutable root = builder.RootElement;
Reading properties from the mutable root
Console.WriteLine($" Name: {root.Name.GivenName} {root.Name.FamilyName}");
Console.WriteLine($" Age: {root.Age}");
Console.WriteLine($" Email: {root.Email}");
Setting properties
root.SetAge(31);
root.SetEmail("john.smith@example.com");
Removing an optional property
bool removed = root.RemoveEmail();
Mutating arrays — add, insert, and remove
// Add a hobby at the end
var hobbies = root.Hobbies;
hobbies.AddItem("cooking");
// Insert a hobby at the beginning (re-fetch after prior mutation)
hobbies = root.Hobbies;
hobbies.InsertItem(0, "painting");
// Remove the hobby at index 1 (re-fetch after prior mutation)
hobbies = root.Hobbies;
hobbies.RemoveAt(1);
Version tracking
The root element is always live after mutations — you can always read the current state from builder.RootElement. Sub-element references (like hobbies above) must be re-fetched from the root after each mutation because the document version changes.
Person current = builder.RootElement;
Console.WriteLine($" Current age: {current.Age}");
Console.WriteLine($" Hobbies count: {current.Hobbies.GetArrayLength()}");
Serialization
// Convert to JSON string (allocates)
string jsonString = builder.RootElement.ToString();
// Write to a Utf8JsonWriter (does not allocate)
ArrayBufferWriter<byte> abw = new();
using (Utf8JsonWriter writer = new(abw))
{
builder.RootElement.WriteTo(writer);
writer.Flush();
}
Running the Example
cd docs/ExampleRecipes/018-CreatingAndMutatingObjects
dotnet run
Related Patterns
- 001-DataObject — Simple data objects with property access, equality, and basic mutation
- 019-CloneAndFreeze — Cloning mutable elements into independent immutable documents