JSON Pointer (RFC 6901): Syntax, Escaping, and JavaScript Resolution

A JSON Pointer (RFC 6901) is a string like /users/0/name that uniquely identifies a single value in a JSON document — used by JSON Patch (RFC 6902), JSON Schema error objects, and the instancePath field in Ajv validation errors. Paths start with / and separate keys with /; literal / in a key is escaped as ~1, and ~ as ~0 — so a key named a/b~c becomes the pointer /a~1b~0c. Array indices are zero-based integers: /items/2 accesses the third element. This guide covers pointer syntax, resolving pointers in JavaScript (the RFC 6901 algorithm), the json-pointer npm package, URI fragment form (#/users/0), relative JSON Pointers, and common uses in JSON Patch and JSON Schema.

Need to validate or inspect JSON documents to trace pointer paths? Jsonic's JSON Formatter lets you explore any document instantly.

Open JSON Formatter

JSON Pointer syntax: tokens, separators, and the empty pointer

A JSON Pointer is either the empty string "" (which refers to the entire document) or a sequence of reference tokens each preceded by a /. Each token names one key in an object or one zero-based integer index in an array. The RFC 6901 grammar in ABNF:

json-pointer    = *( "/" reference-token )
reference-token = *( unescaped / escaped )
unescaped       = %x00-2E / %x30-7D / %x7F-10FFFF
                ; any character except "/" (%x2F) and "~" (%x7E)
escaped         = "~" ( "0" / "1" )
                ; ~0 = literal ~,  ~1 = literal /

3 examples show every rule. Given the document below, 4 pointers are resolved:

{
  "store": {
    "books": [
      { "title": "Clean Code",  "price": 29.99 },
      { "title": "SICP",        "price": 0 }
    ],
    "name": "My/Bookshop"
  }
}

""              → the whole document
"/store"        → { "books": [...], "name": "My/Bookshop" }
"/store/books/0/title"  → "Clean Code"
"/store/books/1/price"  → 0
"/store/name"   → "My/Bookshop"   (no escaping needed here — the / is in the value, not the key)

Note: the / in "My/Bookshop" appears in the value, not in a key, so no escaping is required. Escaping is only needed when / or ~ appear in a key name. The empty pointer "" is valid and refers to the root document — this is distinct from "/", which points to a key literally named the empty string "".

Escaping rules: ~0 and ~1 in depth

JSON Pointer reserves exactly 2 characters that need escaping: / (the path separator) and ~ (the escape character itself). The 2-rule encoding table is:

Character in keyEncoded tokenReason
~~0Escape character must be escaped first
/~1Path separator must be escaped
Anything elseUnchangedAll other Unicode characters are valid tokens

Order matters: when encoding, replace ~ with ~0 first, then replace / with ~1. When decoding, reverse: replace ~1 with / first, then replace ~0 with ~. Reversing the order causes subtle bugs — a token ~01 would decode to / instead of the correct ~1.

// Key name → reference token → full pointer
// Key: "a/b"      → "a~1b"      → "/a~1b"
// Key: "a~b"      → "a~0b"      → "/a~0b"
// Key: "a/b~c"    → "a~1b~0c"   → "/a~1b~0c"
// Key: "~1"       → "~01"       → "/~01"   (~ encoded first → ~0, then 1 unchanged)
// Key: ""         → ""          → "/"      (empty key, accessed by pointer "/")

function encodeToken(key) {
  return key.replace(/~/g, '~0').replace(///g, '~1')
}

function decodeToken(token) {
  return token.replace(/~1/g, '/').replace(/~0/g, '~')
}

// Build a pointer from an array of keys
function buildPointer(keys) {
  if (keys.length === 0) return ''
  return '/' + keys.map(encodeToken).join('/')
}

buildPointer(['store', 'a/b~c', 'price'])
// → "/store/a~1b~0c/price"

A real-world source of ~1 tokens: JSON Schema $defs and OpenAPI 3 $ref values. A reference like "$ref": "#/$defs/My~1Type" points to the key literally named My/Type inside the $defs object — the ~1 represents the slash in the key name. See the JSON Schema guide for more on $ref resolution.

Resolving a JSON Pointer in JavaScript — RFC 6901 algorithm

The resolution algorithm has 4 steps: (1) if the pointer is the empty string, return the whole document; (2) split the pointer on / and discard the first empty segment; (3) decode each token (replace ~1/, then ~0~); (4) walk each token into the current value, treating numeric tokens as array indices. Below is a complete, spec-compliant 15-line implementation:

/**
 * Resolve a JSON Pointer (RFC 6901) against a document.
 * Returns undefined if any segment is missing.
 */
function resolvePointer(doc, pointer) {
  if (pointer === '') return doc
  if (!pointer.startsWith('/')) throw new Error('Invalid JSON Pointer: must start with /')
  const tokens = pointer
    .slice(1)                       // remove leading /
    .split('/')
    .map(t => t.replace(/~1/g, '/').replace(/~0/g, '~'))

  let current = doc
  for (const token of tokens) {
    if (current === null || typeof current !== 'object') return undefined
    if (Array.isArray(current)) {
      const idx = Number(token)
      if (!Number.isInteger(idx) || idx < 0) return undefined
      current = current[idx]
    } else {
      current = current[token]
    }
  }
  return current
}

// Examples
const doc = { store: { books: [{ title: 'Clean Code' }, { title: 'SICP' }] } }
resolvePointer(doc, '')                     // → whole doc
resolvePointer(doc, '/store/books/0/title') // → "Clean Code"
resolvePointer(doc, '/store/books/1/title') // → "SICP"
resolvePointer(doc, '/store/missing')       // → undefined

For production code, prefer the json-pointer npm package (MIT license, zero dependencies, ~6 kB unpacked) which handles edge cases like the "-" sentinel index (meaning "append to array" in JSON Patch) and provides set/remove operations:

npm install json-pointer
import jsonpointer from 'json-pointer'

const doc = { users: [{ name: 'Alice' }, { name: 'Bob' }] }

// Read
jsonpointer.get(doc, '/users/0/name')   // → "Alice"

// Write (mutates the document)
jsonpointer.set(doc, '/users/0/name', 'Carol')
// doc.users[0].name === "Carol"

// Remove
jsonpointer.remove(doc, '/users/1')
// doc.users === [{ name: 'Carol' }]

// Check existence
jsonpointer.has(doc, '/users/0/name')   // → true
jsonpointer.has(doc, '/users/0/email')  // → false

Use Jsonic's JSON formatter to paste and inspect your document while you develop pointer paths — it shows the full structure so you can count array indices and confirm key names.

URI fragment form of a JSON Pointer (#/foo/bar)

RFC 6901 Section 6 defines a second representation: the URI fragment form, where the pointer is prefixed with # and embedded in a URI. This is how JSON Schema uses pointers inside $ref values to cross-reference definitions within the same document. 3 rules govern the fragment form:

// Plain pointer form (used in JSON Patch operations, library calls)
/users/0/name

// URI fragment form (used in JSON Schema $ref, OpenAPI $ref)
#/users/0/name

// When a key contains characters not allowed raw in a URI fragment,
// percent-encode them:
// Key "first name" (space) → "first%20name" in URI fragment
// Pointer: /first name      → Fragment: #/first%20name

// JSON Schema usage — reference a definition
{
  "$ref": "#/$defs/User"      // pointer: /$defs/User
}

// The /$defs/User pointer resolves to the object at:
{
  "$defs": {
    "User": { "type": "object", "properties": { "name": { "type": "string" } } }
  }
}

Characters allowed unescaped in a URI fragment (RFC 3986 unreserved + sub-delimiters + :@!$&'()*+,;=): most ASCII letters, digits, -, _, ., ~, /. Spaces, #, and non-ASCII characters require percent-encoding. The ~0 and ~1 escape sequences from RFC 6901 still apply on top of any percent-encoding — they are separate layers. When passing a pointer to a JavaScript library function, always strip the leading # and undo any percent-encoding first, then pass the plain pointer string.

Use caseForm usedExample
JSON Patch path fieldPlain pointer/users/0/name
Library call (jsonpointer.get)Plain pointer/users/0/name
JSON Schema $refURI fragment#/$defs/User
Ajv instancePath error fieldPlain pointer/users/0/name
OpenAPI $ref within documentURI fragment#/components/schemas/User

JSON Pointer in JSON Patch and JSON Schema

JSON Pointer is the addressing mechanism for 2 major standards. Understanding both use cases is essential for working with modern JSON tooling.

JSON Patch (RFC 6902): Every operation object has a "path" field — a JSON Pointer identifying the location to operate on. The "from" field (used in move and copyoperations) is also a JSON Pointer. 3 example operations:

[
  // Replace the name of the first user
  { "op": "replace", "path": "/users/0/name", "value": "Bob" },

  // Remove the second user entirely
  { "op": "remove", "path": "/users/1" },

  // Add a tag by appending to the tags array ("-" = append sentinel)
  { "op": "add", "path": "/tags/-", "value": "typescript" },

  // Copy a value from one location to another
  { "op": "copy", "from": "/users/0/address", "path": "/billing/address" }
]

The "-" sentinel in /tags/- is a special JSON Pointer token defined by RFC 6902 (not RFC 6901) that means "one past the end of the array" — it is only valid in add operations and cannot be used for reading. See the JSON Patch guide for complete operation reference.

JSON Schema and Ajv: When Ajv validates a document and finds an error, the error object includes an instancePath field that is a JSON Pointer pointing to the failing value, and a schemaPath field that is a JSON Pointer into the schema itself. Example Ajv error:

{
  "instancePath": "/users/0/age",   // pointer into the data being validated
  "schemaPath": "#/properties/users/items/properties/age/type",
  "keyword": "type",
  "params": { "type": "number" },
  "message": "must be number"
}

The instancePath /users/0/age tells you exactly which value failed — the age field of the first element in the users array. You can use jsonpointer.get(data, error.instancePath) to retrieve the offending value programmatically. JSON Schema $ref values that start with # are URI fragment form JSON Pointers into the schema itself — see the JSON Schema guide for details on $defs and $ref resolution.

Relative JSON Pointers — navigating up and across

Relative JSON Pointers are a draft extension (draft-bhutton-relative-json-pointer) that extend RFC 6901 to allow navigation relative to the current position in a document, rather than always from the root. They are used in JSON Schema's unevaluatedProperties, $dynamicRef, and annotation vocabulary. The syntax: a non-negative integer prefix (number of levels to go up) followed optionally by a # (return the current key/index) or a plain JSON Pointer suffix (navigate from that ancestor).

// Current position: /users/3/address/city
// (we are at the "city" key inside address inside the 4th user)

Relative pointer  Resolved to
"0"               /users/3/address/city    (same location)
"0#"              "city"                   (current key name)
"1"               /users/3/address         (one level up)
"1#"              "address"                (key of parent)
"2/name"          /users/3/name            (two up, then /name)
"3"               /users/3                 (three up = the user object)
"3#"              3                        (array index of current user)

Relative JSON Pointers are not part of RFC 6901 — they are a separate Internet Draft that has been adopted by the JSON Schema 2020-12 specification. Most general-purpose JSON Pointer libraries (including the json-pointer npm package) do not support relative pointers; support appears in dedicated JSON Schema validator libraries like Ajv 8 with the ajv-formats plugin. For day-to-day JSON Patch work, you only need absolute RFC 6901 pointers. Relative pointers become relevant primarily when you are authoring complex JSON Schema with cross-referencing annotations.

JSON Pointer vs JSONPath: when to use each

JSON Pointer and JSONPath are frequently confused because both navigate into JSON documents. The difference is fundamental: a JSON Pointer always resolves to exactly 1 value; JSONPath is a query language that returns 0 or more values. The comparison table:

JSON Pointer (RFC 6901)JSONPath
StandardIETF RFC 6901 (2013)RFC 9535 (2024) / Goessner (2007)
Syntax example/store/books/0/title$.store.books[0].title
ReturnsExactly 1 value (or error)0 or more values (nodelist)
WildcardsNoYes: [*], ..*
Filter expressionsNoYes: [?(@.price < 10)]
Recursive descentNoYes: ..
Primary use casesJSON Patch paths, JSON Schema $ref, Ajv instancePathQuerying, extracting, searching documents
Used in RFCsRFC 6902, RFC 7396RFC 9535 (standalone)

Decision rule: If you need to target a single known location for patching, error reporting, or $ref linking — use JSON Pointer. If you need to search, filter, or extract multiple values matching a pattern — use JSONPath. Never use JSONPath where a standard requires JSON Pointer (JSON Patch operations, JSON Schema instancePath) because the semantics are incompatible.

Frequently asked questions

What is a JSON Pointer and what is it used for?

A JSON Pointer is a string syntax defined in RFC 6901 (April 2013) that uniquely identifies a single value inside a JSON document. It looks like /users/0/name — each slash-separated segment is called a "reference token" and navigates one level deeper into the document. An empty string "" points to the entire document. JSON Pointers are used in JSON Patch (RFC 6902) to identify the location of each patch operation, in Ajv validation error objects (the instancePath field), and in JSON Schema $ref references when using URI fragment form (e.g., #/definitions/User). Understanding JSON Pointer is a prerequisite for reading JSON Patch operations and for interpreting validation error messages from any schema validator. For a foundational understanding of the data model being navigated, see the JSON Schema guide.

How do I write a JSON Pointer to access a nested value?

A JSON Pointer starts with a forward slash / followed by reference tokens separated by /. Each token names one key in an object or one zero-based integer index in an array. Given the document {"users": [{"name": "Alice", "age": 30}, {"name": "Bob"}]}, the pointer /users/0/name resolves to "Alice" and /users/1/name resolves to "Bob". To point to the entire users array, use /users. To point to the second element, use /users/1. The pointer "" (empty string) refers to the whole document. Keys must be matched exactly — JSON Pointer is case-sensitive and supports no wildcards, recursive descent, or filter expressions. Those features belong to JSONPath, which is a different standard. Use Jsonic's JSON formatter to explore a document's structure while you develop pointer paths.

How do I escape slashes and tildes in a JSON Pointer?

JSON Pointer uses only 2 escape sequences because / is the path separator and ~ is the escape prefix. The rules: ~ is encoded as ~0, and / is encoded as ~1. These escapes are applied in this order on encoding — replace ~ first, then / — and reversed on decoding — replace ~1 first, then ~0. Example: a key literally named a/b~c becomes the reference token a~1b~0c, so the full pointer is /a~1b~0c. A key named just ~ becomes ~0; a key named just / becomes ~1. Common mistake: encoding / before ~ — this causes a ~ in the original key to produce ~0, whose ~ is then incorrectly re-escaped during a subsequent pass. Always encode in the strict order: ~ ~0, then /~1. This is also how the json-pointer npm package implements encodeToken internally.

What is the difference between a JSON Pointer and a JSONPath expression?

JSON Pointer (RFC 6901) and JSONPath are fundamentally different in scope. JSON Pointer always identifies exactly 1 value — it is an unambiguous address to a single node. JSONPath is a query language that can match 0 or more values, support wildcards ($.users[*].name), recursive descent ($..**), and filter expressions ($.users[?(@.age > 18)]). JSON Pointer syntax: /users/0/name. Equivalent JSONPath: $.users[0].name. JSON Pointer is used by RFCs (6902, 7396, 6901) precisely because it is unambiguous — every operation in a JSON Patch document needs to target exactly 1 location. JSONPath is used when you need to search or extract multiple values from a document. If you need to locate a single field precisely (for patching, error reporting, $ref), use JSON Pointer. If you need to query or filter, use JSONPath.

How do I resolve a JSON Pointer in JavaScript?

The RFC 6901 resolution algorithm splits the pointer on / and walks each token into the document. In JavaScript, implement it in about 10 lines: split the pointer string on /, skip the first empty segment, decode each token (replace ~1 with / and ~0 with ~, in that order), and use each token as a property key or array index. The json-pointer npm package (MIT license) provides jsonpointer.get(obj, '/users/0/name') for reading, jsonpointer.set(obj, '/users/0/name', value) for writing, and jsonpointer.remove(obj, '/users/0') for deletion. For a zero-dependency read-only solution, the core logic is: pointer.split('/').slice(1).reduce((doc, t) => doc[t.replace(/~1/g,'/').replace(/~0/g,'~')], document). Be aware that library set/remove operations mutate the target object in place; create a deep copy first if you need to preserve the original.

What is the URI fragment form of a JSON Pointer?

The URI fragment form uses # followed by the pointer with percent-encoding applied to characters not allowed raw in a URI fragment. For the common cases — letters, digits, /, ~, -, _ — no percent-encoding is needed. Example: the pointer /users/0/name becomes the URI fragment #/users/0/name. This form is used in JSON Schema $ref values to reference definitions within the same document: "$ref": "#/$defs/User" points to the value at the /$defs/User path. When a key contains a space or other special character, percent-encode it in the fragment form: a key named first name becomes first%20name. The URI fragment form is described in RFC 6901 Section 6. When passing a pointer to a library function like jsonpointer.get(), always use the plain form without the # prefix — the fragment form is only for embedding in URIs and $ref strings. See the JSON Schema guide for more on using $ref and $defs.

Ready to work with JSON Pointer?

Use Jsonic's JSON Formatter to inspect your documents and trace pointer paths visually. Need to apply patches using JSON Pointer paths? Try the JSON Patch guide next.

Open JSON Formatter