• RFC 9535
  • Jayway dialect
  • JSON Pointer
  • Zero dependencies
  • Swift 6
  • MIT

JSONPath for Swift.
Both dialects. No dependencies.

Query, point into and rewrite JSON with the IETF standard or with the Jayway syntax your users already know. Pure Swift with no Foundation and no imports, so it runs anywhere Swift runs: iOS apps, macOS tools, Linux servers.

Query.swift
import JSONPath

let document = try JSONValue(parsing: json)

// RFC 9535: all 715 compliance cases pass
try JSONPath("$.store.book[?@.price < 10].title")
    .values(in: document)
// ["Sayings of the Century", "Moby Dick"]

// The Jayway dialect, path functions included
try JSONPath("$.store.book.length()", dialect: .jayway)
    .values(in: document)  // [3]

// Rewrite every match, key order kept
var copy = document
try copy.replace(JSONPath("$..price"), with: 0)

What is JSONPath

XPath for JSON, now an IETF standard.

A JSONPath expression selects values from a JSON document, the way a CSS selector picks elements from a page. It started as an informal proposal, spread through Jayway JsonPath in Java, and became RFC 9535 in 2024. On the bookstore document from the README:

RFC 9535
$..price
selects
[8.95, 8.99, 22.99, 19.95]

Every price, at any depth.

RFC 9535
$.store.book[?@.price < 10].title
selects
["Sayings of the Century", "Moby Dick"]

A filter on each book, then a member of the ones that pass.

Jayway
$.store.book.length()
selects
[3]

A path function, from the dialect most existing queries are written in.

Each result comes with its location as a normalized path ($['store']['book'][0]['title']) or a JSON Pointer (/store/book/0/title), so you can go back and change it.

JSON Pointer

JSONPath finds values.
JSON Pointer says exactly where one is.

RFC 6901 is the addressing format the JSON ecosystem already speaks: $ref in JSON Schema and OpenAPI, path in JSON Patch, the locations validators report errors at. A pointer names one location, token by token, so resolving it is a walk down the document, never a search.

Tokens
/store/book/0/title
resolves to
"Sayings of the Century"

Each token is a member name, or an index when the value is an array.

Escaping
/a~1b/m~0n
has the tokens
["a/b", "m~n"]

~1 stands for / and ~0 for ~. Build a pointer from tokens and it escapes them for you.

Fragment
#/store/bicycle
is the same as
/store/bicycle

The URI fragment form, as written after the # of a $ref, is accepted.

JSONPathJSON Pointer
Selectsany number of nodes, from a searchexactly one location, or nothing
Looks like$..book[?@.price < 10]/store/book/0/price
StandardRFC 9535 (and the Jayway dialect)RFC 6901
Use it toquery, filter and extractstore, report, reference and patch a location
In swift-jsonpathJSONPath, NormalizedPathJSONPointer
  • From a query to pointers. Every node a query returns carries its normalized path, and .jsonPointer turns it into a pointer: search once, then store, report or patch each match by its pointer.
  • Set by pointer. set(_:at:) creates a missing object member and appends to an array with -, like JSON Patch's add. A missing parent or an index past the end throws a MutationError instead of guessing.
  • Strict where the RFC is. ~ must be followed by 0 or 1, and a non-empty pointer starts with /: anything else throws a JSONPointer.ParseError. An array index with leading zeros never matches.
  • A value you can keep. JSONPointer is Hashable, Sendable and Codable as its string form, so a field like let location: JSONPointer fits in your own models.
Pointers.swift
// From a query's results to pointers
try JSONPath("$..price").paths(in: document).map(\.jsonPointer.description)
// ["/store/book/0/price", "/store/book/1/price", "/store/book/2/price", "/store/bicycle/price"]

// Setting a value: missing object members are created, and `-` appends to an array
var copy = document
try copy.set("red", at: JSONPointer("/store/bicycle/color"))
try copy.set(["title": "Dune", "price": 9.99], at: JSONPointer("/store/book/-"))

Features

Everything you need to work with JSON by path.

One small package: the query languages, the JSON model they run on, and the tools around them.

RFC 9535, to the letter

The strict grammar, well-typedness checked when the path is parsed, and the length count match search value functions with I-Regexp. Passes all 715 cases of the official compliance suite.

The Jayway dialect, verified

[?(…)] filters, loose comparisons, =~ in contains anyof and the other operators, and path functions like .avg(). Passes Sextant's whole test suite. JSONPath(anyDialect:) accepts whichever a user types.

Mutations and JSON Pointer

Replace, remove or transform every node a path selects. Address one value with an RFC 6901 JSONPointer, convert a query's results into pointers, and set values by pointer, appending with -.

An order-preserving JSON model

JSONValue keeps object members in document order through parsing, querying, mutation and printing. A strict RFC 8259 parser, a pretty printer, and a Codable bridge that keeps key order too.

Built for editors

Token ranges for syntax colouring in both dialects, even on an expression that is still being typed. Typed errors with the position of the problem, and JSONPath.validate for a quick yes or no.

Zero dependencies, Swift 6

No Foundation and no packages: on Linux it links only the standard library and libm. Strict concurrency, and every public type is a Sendable value.

Two dialects, one API

The standard for new queries. Jayway for the ones you already have.

Pick the dialect per path. Both return the same JSONValue results and normalized paths, and the differences between them are written down, not discovered in production.

Feature.rfc9535.jayway
Names, wildcards, indices, slices, unions, descendants✓✓
Filters[?expr]; on objects, tests the members[?(expr)] / [?expr]; on objects, tests the object itself
Comparisonsstrict and typedloose ('1' == 1), plus === / !==
Functions in filterslength count match search valuesame
Jayway operators–=~ /re/flags, in, nin, contains, size, empty, anyof, …
Path functions–length() avg() sum() min() max() concat() keys() …
Implicit root (store.book)–✓
Relation on a missing pathfalseerror (the filter fails)

The complete table is in the README.

Code tour

Short, typed, and checked by the compiler.

These snippets come from the README, which the test suite compiles and runs, so they stay correct.

Parse a document, then ask a path for values, normalized paths or both. Filters, descendants and the RFC 9535 functions work as the standard says.

import JSONPath

let document = try JSONValue(parsing: """
    {"store": {"book": [
      {"category": "reference", "author": "Nigel Rees", "title": "Sayings of the Century", "price": 8.95},
      {"category": "fiction", "author": "Herman Melville", "title": "Moby Dick", "isbn": "0-553-21311-3", "price": 8.99},
      {"category": "fiction", "author": "J. R. R. Tolkien", "title": "The Lord of the Rings", "price": 22.99}
    ], "bicycle": {"color": "red", "price": 19.95}}}
    """)

let cheap = try JSONPath("$.store.book[?@.price < 10].title")
try cheap.values(in: document)         // ["Sayings of the Century", "Moby Dick"]
try cheap.paths(in: document)          // [$['store']['book'][0]['title'], $['store']['book'][1]['title']]
try cheap.nodes(in: document)          // locations + values
try document.query("$..price")         // [8.95, 8.99, 22.99, 19.95]

try JSONPath("$.store.book[?match(@.author, '.*Tolkien')].title").values(in: document)
try JSONPath("$.store.book[?count(@.isbn) > 0].title").values(in: document)

Built for

Apps, tools and servers that read JSON they did not design.

Configuration and remote data

Pull the values you need out of a large or changing payload with one expression instead of a tower of Codable types, and decode just that part into your model.

API testing and workflows

Assert on responses and extract values between steps. swift-arazzo evaluates OpenAPI Arazzo JSONPath criteria with it.

Developer tools and editors

A query field that accepts both dialects, validates as you type and colours the expression, with results you can jump to by normalized path or JSON Pointer.

Porting from Java, Android or Sextant

Existing Jayway queries keep their meaning: loose comparisons, operators and path functions behave as Jayway's, checked against Sextant's own tests.

Quality and speed

Checked against the standard and against Jayway. Measured on every change.

715 / 715cases of the official JSONPath Compliance Test Suite passed, in RFC 9535 mode
322tests from Sextant's suite, ported to Swift Testing, pin the Jayway dialect
0dependencies and 0 imports: not even Foundation
4CI builds on every change: Linux on Swift 6.0, 6.1 and 6.2, and macOS, plus API-breakage and DocC checks

A differential run against Sextant agrees on every input except where Sextant itself is buggy; those differences are commented in the tests.

Benchmark on a 1.4 MB document (p50, Apple silicon)0.0.40.1.0
Parse the JSON text26.4 ms7.9 ms
Run 8 RFC 9535 queries, from $.store.book[0].title to $..*117 ms15.3 ms
Run the same 8 queries in the Jayway dialect66.7 ms9.3 ms
Find every author with $..author9.9 ms1.3 ms
Replace all 5,000 prices with $..price419 ms10.0 ms
Remove the books a filter selects111 ms2.4 ms

Benchmarks use package-benchmark and live in the repository: swift package --package-path Benchmarks benchmark.

Install

Add it to your package.

Package.swift
dependencies: [
    .package(url: "https://github.com/mackoj/swift-jsonpath.git", from: "0.1.0"),
],
targets: [
    .target(name: "MyTool", dependencies: [.product(name: "JSONPath", package: "swift-jsonpath")]),
]
  • Swift 6.0+ on Linux, macOS 13+, iOS 16+, tvOS 16+, watchOS 9+ and visionOS 1+.
  • One product, JSONPath: both dialects, JSONValue, JSON Pointer, mutations and the Codable bridge.
  • API reference on the Swift Package Index; guide and examples in the README.