Skip to content
SoftCreatRPublic

Repository files navigation

JSONPath for PHP 8.3+

Build Latest Release MIT licensed Plant Tree Codecov branch

This is a JSONPath implementation for PHP with an opt-in RFC 9535 mode and a backward-compatible default mode. Both use cached queries and evaluate expressions without eval.

Highlights

  • PHP 8.3+ with typed class constants, enums, readonly tokens, #[Override], and no eval; CI covers PHP 8.3 through the PHP 8.6 nightly.
  • Works with arrays, objects, and ArrayAccess/traversables in any combination.
  • Supports child and descendant selectors, wildcards, quoted name unions, indexes, negative indexes, and array slices.
  • Filters support existence tests (including descendant and nested filters), path-to-path/root comparisons, deep equality, and distinct RFC Nothing versus JSON null semantics.
  • Malformed and non-portable legacy syntax is rejected instead of being interpreted as a different valid query.
  • RFC mode passes all 715 cases in the pinned JSONPath compliance suite, including normalized result paths and rejection of invalid selectors.

Installation

Requires PHP 8.3 or newer.

composer require softcreatr/jsonpath:"^2.0"

Development

Useful commands:

composer exec phpunit
composer phpstan
composer cs

The PHPUnit suite includes the pinned JSONPath Compliance Test Suite. Its 715 cases check both returned values and normalized paths, preserve objects versus arrays, and accept alternative node orderings only where the fixtures explicitly allow them. Tests run offline. CI verifies 100% class, method, and line coverage with Xdebug. Code style uses PHP CS Fixer.

RFC 9535 mode

Enable JSONPath::RFC9535 for standard query syntax and semantics. Existing calls without this flag keep their previous behavior.

use Flow\JSONPath\JSONPath;

$data = json_decode(
    '{"books":[{"title":"First","price":5},{"title":"Second","price":15}]}',
    associative: false,
    flags: JSON_THROW_ON_ERROR,
);

$query = new JSONPath($data, JSONPath::RFC9535);

$query->find('$.books[?@.price < 10].title')->getData();
// ['First']

$query->findPaths('$.books[?@.price < 10].title');
// ["$['books'][0]['title']"]

$query->find('$.books[*,0].title')->getData();
// ['First', 'Second', 'First']

Decode JSON objects as objects, as above. PHP lists represent JSON arrays; objects represent JSON objects. Associative PHP arrays, magic getters, and ArrayAccess belong to the compatibility API. RFC mode reads public object members and does not invoke magic accessors, even when ALLOW_MAGIC is also set.

RFC mode supports every selector kind in an ordered union, optional filter parentheses, Unicode member shorthands, JSON string escapes, negative and zero slice steps, logical precedence, and all five standard function extensions. Queries are parsed and type-checked before evaluation, including when the document is empty or a preceding segment has no matches. Invalid syntax raises JSONPathException.

The flag is retained by returned JSONPath objects. Compiled queries are cached separately for each mode. findPaths() always uses RFC mode and returns one normalized path per match in node-list order, including duplicates. It does not change the document or the values returned by find().

The default mode retains its existing extensions and restrictions, including rootless member queries, trimmed surrounding whitespace, trailing-comma unions, and the older filter parser. Choose RFC mode when queries must follow the standard; passing the community comparison suite alone does not establish RFC compliance.

JSONPath Examples

JSONPath Result
$.store.books[*].author the authors of all books in the store
$..author all authors
$.store..price the price of everything in the store.
$..books[2] the third book
$..books[-1:] the last book in order.
$..books[0,1] the first two books
$..books['title','year'] multiple keys in a union
$..books[:2] the first two books
$..books[::2] every second book starting from first one
$..books[1:6:3] every third book starting from 1 till 6
$..books[?(@.isbn)] filter all books with isbn number
$..books[?(@.price<10)] filter all books cheaper than 10
$..* all elements in the data (recursively extracted)

Expression syntax

Symbol Description
$ The root object/element
@ The current object/element inside a filter
. or [] Child operator
.. Recursive descent
* Wildcard. All child elements regardless their index.
[,] Array indexes or quoted member names as a set
[start:end:step] Array slice operator borrowed from ES4/Python.
?() Filters a node list by an existence or comparison expression

PHP Usage

Using arrays

<?php
require_once __DIR__ . '/vendor/autoload.php';

$data = ['people' => [
    ['name' => 'Sascha'],
    ['name' => 'Bianca'],
    ['name' => 'Alexander'],
    ['name' => 'Maximilian'],
]];

print_r((new \Flow\JSONPath\JSONPath($data))->find('$.people.*.name')->getData());

/*
Array
(
    [0] => Sascha
    [1] => Bianca
    [2] => Alexander
    [3] => Maximilian
)
*/

Using objects

<?php
require_once __DIR__ . '/vendor/autoload.php';

$data = json_decode('{"name":"Sascha Greuel","birthdate":"1987-12-16","city":"Gladbeck","country":"Germany"}', false);

print_r((new \Flow\JSONPath\JSONPath($data))->find('$')->getData()[0]);

/*
stdClass Object
(
    [name] => Sascha Greuel
    [birthdate] => 1987-12-16
    [city] => Gladbeck
    [country] => Germany
)
*/

Magic method access

The options flag JSONPath::ALLOW_MAGIC will instruct JSONPath when retrieving a value to first check if an object has a magic __get() method and will call this method if available. This feature is iffy and not very predictable as:

  • wildcard and recursive features will only look at public properties and can't smell which properties are magically accessible
  • there is no property_exists check for magic methods so an object with a magic __get() will always return true when checking if the property exists
  • any errors thrown or unpredictable behavior caused by fetching via __get() is your own problem to deal with
<?php

use Flow\JSONPath\JSONPath;

$myObject = (new Foo())->get('bar');
$jsonPath = new JSONPath($myObject, JSONPath::ALLOW_MAGIC);

Filter expressions

Filter evaluation is intentionally limited to portable, eval-free operations:

  • Comparisons support ==, !=, <, <=, >, and >=.
  • Logical expressions support !, &&, ||, and grouped subexpressions.
  • Singular current/root queries can be compared; non-singular, descendant, and nested-filter queries can be used as existence tests.
  • Missing nodes use RFC Nothing semantics and are not conflated with JSON null.
  • RFC 9535 function expressions are parsed, statically type-checked, and evaluated without eval() or dynamic function dispatch.

Examples:

[?(@.title == "A string")]      // equality
[?(@.price < 10)]               // numeric comparison
[?(@.key == @.other)]           // path-to-path comparison
[?(@.key == $.rootValue)]       // root reference
[?(@.isbn)]                     // singular existence test
[?(@..child)]                   // descendant existence test
[?(@.items[?(@.price > 10)])]   // nested-filter existence test
[?(@['weird-key']=="ok")]       // bracket-escaped member

RFC 9535 function extensions

The following examples use this shared document:

$data = [
    ['name' => 'Ada', 'tags' => ['php', 'json'], 'timezone' => 'Europe/Berlin', 'colors' => ['red']],
    ['name' => 'Bob', 'tags' => ['php'], 'timezone' => 'America/Toronto', 'colors' => ['blue', 'green']],
    ['name' => '李', 'tags' => [], 'timezone' => 'Asia/Tokyo', 'colors' => []],
];

length()

Returns the number of Unicode scalar values in a string, elements in an array, or members in an object. Other values produce Nothing.

(new JSONPath($data))->find('$[?length(@.name) == 3].name')->getData();
// ['Ada', 'Bob']

count()

Returns the number of nodes selected by a query, including nodes whose value is null.

(new JSONPath($data))->find('$[?count(@.tags[*]) >= 2].name')->getData();
// ['Ada']

match()

Returns true when the entire input string matches an RFC 9485 I-Regexp.

(new JSONPath($data))->find('$[?match(@.timezone, "Europe/.*")].name')->getData();
// ['Ada']

search()

Returns true when any substring of the input string matches an RFC 9485 I-Regexp.

(new JSONPath($data))->find('$[?search(@.name, "ob")].name')->getData();
// ['Bob']

value()

Returns the value of a nodelist containing exactly one node. Empty or multi-node nodelists produce Nothing.

(new JSONPath($data))->find('$[?value(@.colors[*]) == "red"].name')->getData();
// ['Ada']

Function calls are checked against the RFC ValueType, LogicalType, and NodesType rules before evaluation. Invalid I-Regexps evaluate to logical false.

Script expressions, synthetic .length, legacy regular-expression/membership operators, and array/object literals in comparisons are not supported. Unsupported or ill-typed syntax raises JSONPathException.

A full list of comparison cases can be found in the JSONPath Comparison Cheatsheet.

Similar projects

FlowCommunications/JSONPath is the predecessor of this library by Stephen Frank

Other / Similar implementations can be found in the Wiki.

Changelog

A list of changes can be found in the CHANGELOG.md file.

License 🌳

MIT © 1-2.dev

This package is Treeware. If you use it in production, then we ask that you buy the world a tree to thank us for our work. By contributing to the ecologi project, you’ll be creating employment for local families and restoring wildlife habitats.

Contributors ✨

Sascha
Sascha Greuel
James
James Lucas
Fabian
Fabian Blechschmidt
Mikko
Mikko Pesari
warlof/
warlof
Sergey
Sergey G
Alexandru
Alexandru Pătrănescu
Oleg
Oleg Andreyev
Remy
Remy Suen
esomething/
esomething

Releases

Sponsor this project

Used by

Contributors

Languages