Skip to content
Matthew Adams By Matthew Adams Co-Founder · 3 min read
Introducing Corvus.Text.Json V5: JSON Pointer - Zero-Allocation Path Resolution

In the previous post, we saw how JSON Patch uses paths like "/address/city" to target specific locations in a document. Those paths follow RFC 6901 JSON Pointer, a standard syntax for addressing values within a JSON document.

V5 exposes this as a first-class API through Utf8JsonPointer. It lets you resolve a path against any IJsonElement<T> and get back a typed result, with no allocation.

What is JSON Pointer?

A JSON Pointer is a string that identifies a specific value in a JSON document. It uses / as a path separator, with two escape sequences: ~0 for ~ and ~1 for /.

Pointer Meaning
"" The root document
"/name" The name property of the root object
"/address/city" Nested property access
"/tags/0" First element of the tags array
"/a~1b" Property named a/b (escaped forward slash)
"/m~0n" Property named m~n (escaped tilde)

JSON Pointer is used throughout the JSON ecosystem. JSON Patch (RFC 6902) uses it for all path and from fields. JSON Schema uses it in $ref URI fragments. OpenAPI uses it to reference components. If you work with JSON tooling, you will encounter JSON Pointer paths regularly.

Resolving a pointer

Utf8JsonPointer is a readonly ref struct that wraps a ReadOnlySpan<byte>. It validates the pointer syntax on creation and resolves it against any IJsonElement<T>:

using var doc = ParsedJsonDocument<JsonElement>.Parse("""
    {
        "store": {
            "name": "Book Shop",
            "books": [
                { "title": "JSON at Work", "price": 29.99 },
                { "title": "Schema Design", "price": 34.50 }
            ]
        }
    }
    """);

JsonElement root = doc.RootElement;

// Resolve a nested property
if (Utf8JsonPointer.TryCreateJsonPointer("/store/name"u8, out Utf8JsonPointer pointer)
    && pointer.TryResolve<JsonElement, JsonElement>(root, out JsonElement name))
{
    Console.WriteLine(name.GetString()); // "Book Shop"
}

// Resolve an array element
if (Utf8JsonPointer.TryCreateJsonPointer("/store/books/1/title"u8, out Utf8JsonPointer bookPointer)
    && bookPointer.TryResolve<JsonElement, JsonElement>(root, out JsonElement title))
{
    Console.WriteLine(title.GetString()); // "Schema Design"
}

The resolution walks the document's internal metadata table directly. There is no intermediate string allocation, no tree of path objects, and no dictionary lookup. The pointer's ReadOnlySpan<byte> slices are compared against the document's UTF-8 property names byte-by-byte.

Why a ref struct?

Utf8JsonPointer is a readonly ref struct because it holds a ReadOnlySpan<byte> that points directly into the source buffer. This means it cannot be stored on the heap, boxed, or used as a field in a class. It exists for the duration of the stack frame where you use it.

This is a deliberate design choice. A JSON Pointer is typically created, resolved, and discarded in a single operation. Making it a ref struct means zero GC pressure for this pattern.

If you need to store a pointer for later use, store the raw byte[] or string and create the Utf8JsonPointer when you need to resolve it.

Source location resolution

One of the more unusual features of Utf8JsonPointer is the ability to resolve a path to a source location in the original document. This is useful for tooling that needs to produce error messages with line numbers and column offsets.

using var doc = ParsedJsonDocument<JsonElement>.Parse("""
    {
        "name": "Alice",
        "age": 30
    }
    """);

if (Utf8JsonPointer.TryCreateJsonPointer("/age"u8, out Utf8JsonPointer pointer)
    && pointer.TryGetLineAndOffset(doc.RootElement, out int line, out int charOffset, out long lineByteOffset))
{
    Console.WriteLine($"'age' is at line {line}, column {charOffset}");
    // 'age' is at line 3, column 12
}

TryGetLineAndOffset first resolves the pointer to find the target element, then walks the document's raw UTF-8 buffer to compute the 1-based line number and character offset. This is the same mechanism that schema validation uses to produce diagnostic locations.

Segment decoding

If you need to process pointer segments individually, DecodeSegment handles the ~0 and ~1 unescaping:

ReadOnlySpan<byte> encoded = "a~1b~0c"u8; // represents "a/b~c"
Span<byte> decoded = stackalloc byte[encoded.Length];
int written = Utf8JsonPointer.DecodeSegment(encoded, decoded);
// decoded[..written] contains "a/b~c"

This is useful when you are building tooling that iterates over the segments of a pointer for custom processing, rather than resolving it against a document.

Relationship with JSON Patch

In the JSON Patch post, we showed the PatchBuilder API for applying RFC 6902 operations. Internally, every path and from field in a patch operation is resolved using Utf8JsonPointer. The two APIs share the same underlying resolution logic, so the path syntax, escaping rules, and performance characteristics are identical.

If you are building tooling that needs to inspect or manipulate JSON Patch documents programmatically, Utf8JsonPointer gives you direct access to the resolution mechanism.

Next up

In the next post, we'll look at V5's extended type system. It covers UTF-8 URIs and IRIs, arbitrary-precision numerics with BigNumber, and first-class NodaTime integration.

FAQs

What is JSON Pointer? RFC 6901 JSON Pointer is a string syntax for identifying a specific value within a JSON document. A pointer like /address/city means 'the city property inside the address property of the root object'. It uses / as a separator and ~ escaping for special characters.
How does Utf8JsonPointer relate to JSON Patch? JSON Patch (RFC 6902) uses JSON Pointer syntax for all its path and from fields. Internally, V5's PatchBuilder resolves paths using the same Utf8JsonPointer mechanism described in this post.
Does Utf8JsonPointer allocate? No. Utf8JsonPointer is a readonly ref struct that holds a ReadOnlySpan pointing into the original UTF-8 buffer. Resolution walks the document's metadata table without allocating.

Matthew Adams

Co-Founder

Matthew Adams

Matthew was CTO of a venture-backed technology start-up in the UK & US for 10 years, and is now the co-founder of endjin, which provides technology strategy, experience and development services to its customers who are seeking to take advantage of Microsoft Azure and the Cloud.