Data and Formats · Lesson 19

JsonElement lifetime and Clone

Return independent JSON data instead of a view owned by a disposed document.

How it works

JsonDocument owns the storage used by its JsonElement views and must be disposed when its lifetime ends. A JsonElement is a struct, but copying that struct does not automatically copy the JSON storage it refers to. Returning an element can therefore accidentally return a view whose owner has already been disposed.

Load(false) selects the item element and returns it directly. The using scope disposes the document before the caller reads name, so accessing the borrowed view fails. Load(true) clones the selected element while the document is alive, producing an element the caller can use independently after that document's scope ends.

This is another example of the difference between value copying and ownership copying. A small value can contain references to externally owned storage. The correct return contract must explain whether the caller receives independent data or a borrowed view that remains valid only while a separately owned resource is alive.

Do not keep every JsonDocument alive forever just to avoid learning its lifetime rules. If a caller needs to own the entire document, return it and transfer disposal responsibility explicitly. If a caller only needs one scalar value, extract that value before disposal. Clone when an independent JSON subtree is the useful result.

Practice by returning a string name directly from a helper that disposes its document. Compare that safe scalar extraction with returning a borrowed JsonElement. Then clone only the nested item rather than the whole root and verify that unrelated sibling data is not needed by the caller.

Finally, design a small parsing API whose name and return type make ownership clear. Tests should access its result after the helper has returned, because a test that reads only inside the original using block cannot detect the lifetime bug. Resource ownership remains important even when the exposed type looks like a lightweight value.

A working example

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;

try { Console.WriteLine(Load(false).GetProperty("name").GetString()); }
catch (ObjectDisposedException) { Console.WriteLine("disposed"); }
Console.WriteLine(Load(true).GetProperty("name").GetString());
static JsonElement Load(bool clone) {
    using var document = JsonDocument.Parse("""{"item":{"name":"rune"}}""");
    JsonElement item = document.RootElement.GetProperty("item");
    return clone ? item.Clone() : item;
}

Expected output

disposed
rune
Try this
Clone the element before the owning JsonDocument leaves its using scope.