Home JsonPointer.Net Basics
JsonPointer.Net Basics
Cancel

JsonPointer.Net Basics

JSON Pointer is a syntax that allows you to isolate a single element within a JSON document by navigating down a series of object properties and array indices.

Syntax

The syntax is really simple:

1
/objects/and/3/arrays

This pointer has four segments. Each segment specifies either an object property or, if the segment is a number, an array index. Interestingly, the 3 above could be either an object property or an array index. There’s nothing about the pointer that specifies a distinction. It will resolve for both of these documents:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
  "objects": {
    "and": [
      "item zero",
      null,
      2,
      {
        "arrays": "found me"
      }
    ]
  }
}

{
  "objects": {
    "and": {
      "3": {
        "arrays": "found me"
      }
    }
  }
}

If a property contains a /, it must be escaped by replacing it with ~1. Additionally, the escape character ~ must be escaped by replacing it with ~0.

It also supports a URL format, which is essentially the same thing, except that it starts with a #, then followed by the standard pointer. This format also will %-encode any URL-reserved characters, like = and ?.

In code

The JsonPointer class is the model for JSON Pointer.

There are three ways create pointers:

  • parsing with either Parse() or TryParse()
    1
    
    var pointer = JsonPointer.Parse("/objects/and/3/arrays");
    
  • building with Create() and supplying the segments explicitly
    1
    
    var pointer = JsonPointer.Create("object", "and", 3, "arrays");
    
  • building with Create<T>() and supplying a LINQ expression (also see below)
    1
    
    var pointer = JsonPointer.Create<MyObject>(x => x.objects.and[3].arrays);
    

All of these options will give you an instance of the model that can be used to evaluate JSON data.

1
2
3
using var element = JsonDocument.Parse("{\"objects\":{\"and\":[\"item zero\",null,2,{\"arrays\":\"found me\"}]}}");
var result = pointer.Evaluate(element);
// result contains a JsonElement with a "found me" value

or

1
2
3
4
var element = JsonNode.Parse("{\"objects\":{\"and\":[\"item zero\",null,2,{\"arrays\":\"found me\"}]}}");
var success = pointer.TryEvaluate(element, out var result);
// success is true
// result contains a JsonNode with a "found me" value

The designers of the JsonNode API have elected (for reasons I disagree with) to consider JSON null and .Net null to be equivalent. This goes against both my personal experience building Manatee.Json and the JsonElement API, both of which maintain a separation between these two null concepts. Because of the unified design, it’s impossible to determine whether a returned JsonNode value of null represents a value that is present but null or it is merely absent from the data. To accommodate this, the evaluation method can only support the familiar TryParse() signature. A return of true indicates the value was found, and false indicates it was not. In the case of a true return, result may still be null, indicating the value was found and was a JSON null.

Pointer math

You can also combine and augment pointers in different ways.

Joining two pointers together:

1
2
3
var pointer1 = JsonPointer.Parse("/objects/and");
var pointer2 = JsonPointer.Parse("/3/arrays");
var final = pointer1.Combine(pointer2);

Appending additional segments to an existing pointer:

1
2
var pointer = JsonPointer.Parse("/objects/and");
var final = pointer1.Combine(3, "arrays");

Access pointer parts and create sub-pointers

You can retrieve the individual segments using the indexer:

1
2
var pointer = JsonPointer.Parse("/objects/and/3/arrays");
var andSegment = pointer[1];  // "and" (string)

If you’re using .Net 8 or higher, the indexer also supports Range values, so you can obtain a new pointer containing a portion of the segments.

Get the immediate parent:

1
2
var pointer = JsonPointer.Parse("/objects/and/3/arrays");
var parent = pointer[..^1];  // /objects/and/3

Or get the local pointer (imagine you’ve navigated to /objects/and/ and you need the pointer relative to where you are):

1
2
var pointer = JsonPointer.Parse("/objects/and/3/arrays");
var local = pointer[^2..];  // /3/arrays

There are also method versions of this functionality, which are also available if you’re not yet using .Net 8: .GetAncestor(int) and .GetLocal().

Accessing pointers acts like accessing strings: getting segments has no allocations (like getting a char via the string’s int indexer), but creating a sub-pointer does allocate a new JsonPointer instance (like creating a substring via the string’s Range indexer).

Building pointers using Linq expressions

When building a pointer using the Create<T>() method which takes a Linq expression, there are a couple of things to be aware of.

First, JSON Pointer supports using - as a segment to indicate the index beyond the last item in an array. This has several use cases including creating a JSON Patch to add items to arrays.

Secondly, you have some name transformation options at your disposal.

The first way to customize your pointer is by using the [JsonPropertyName] attribute to provide a custom name. Since this attribute controls how System.Text.Json serializes the property, this attribute will override any other options.

The second way to customize your pointer is by providing a PointerCreationOptions object as the second parameter. Currently there is only the single option: PropertyNamingResolver. This property is a function that takes a MemberInfo and returns the string to use in the pointer. Several presets have been created for you and are available in the PropertyNamingResolvers static class:

NameSummary
AsDeclaredMakes no changes. Properties are generated with the name of the property in code.
CamelCaseProperty names to camel case (e.g. camelCase).
KebabCaseProperty names to kebab case (e.g. Kebab-Case).
PascalCaseProperty names to pascal case (e.g. PascalCase).
SnakeCaseProperty names to snake case (e.g. Snake_Case).
UpperKebabCaseProperty names to upper kebab case (e.g. UPPER-KEBAB-CASE).
UpperSnakeCaseProperty names to upper snake case (e.g. UPPER_SNAKE_CASE).

Relative JSON Pointers

JSON Hyperschema relies on a variation of JSON Pointers called Relative JSON Pointers that also includes the number of parent and/or array-index navigations. This allows the system to start at an internal node in the JSON document and navigate to another node potentially on another subtree.

Relative JSON Pointers are implemented with the RelativeJsonPointer struct. Interactions with this struct are very similar to JsonPointer.

Since evaluation of these pointers require parent navigation, a feature which is unsupported by JsonElements, only the JsonNodes can be processed.

Ahead of Time (AOT) compatibility

JsonPointer.Net v4 includes updates to support Native AOT applications. However because everything in this library is handled via parsing and direct-to-string output, you don’t need to do anything.

You’re done. Congratulations.

Contents