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()
orTryParse()
1
var pointer = JsonPointer.Parse("/objects/and/3/arrays");
- building with
Create()
and supplying the segments explicitly1
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 theJsonElement
API, both of which maintain a separation between these two null concepts. Because of the unified design, it’s impossible to determine whether a returnedJsonNode
value ofnull
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 familiarTryParse()
signature. A return oftrue
indicates the value was found, andfalse
indicates it was not. In the case of atrue
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’sint
indexer), but creating a sub-pointer does allocate a newJsonPointer
instance (like creating a substring via the string’sRange
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:
Name | Summary |
---|---|
AsDeclared | Makes no changes. Properties are generated with the name of the property in code. |
CamelCase | Property names to camel case (e.g. camelCase ). |
KebabCase | Property names to kebab case (e.g. Kebab-Case ). |
PascalCase | Property names to pascal case (e.g. PascalCase ). |
SnakeCase | Property names to snake case (e.g. Snake_Case ). |
UpperKebabCase | Property names to upper kebab case (e.g. UPPER-KEBAB-CASE ). |
UpperSnakeCase | Property 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 JsonElement
s, only the JsonNode
s 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.