symfony/json-path
Symfony JsonPath is a PHP component for navigating and querying JSON using JSONPath syntax per RFC 9535. Use JsonCrawler to evaluate paths, filter expressions, and extract values from complex JSON documents.
Installation
composer require symfony/json-path:^8.1.9
No additional configuration is required—autoloading handles the rest. Pin to ^8.1.9: the 8.1.2–8.1.8 range contains escaping/anchor bugs in the match()/search() filter functions and drops null elements from index/slice selectors.
First Use Case: Extracting Nested Data
use Symfony\Component\JsonPath\JsonPath;
$json = '{"user": {"profile": {"name": "John", "age": 30}}}';
$jsonPath = new JsonPath();
$name = $jsonPath->evaluate('$.user.profile.name', json_decode($json, true));
// Returns: "John"
Where to Look First
evaluate(): Single result (or null).evaluateAll(): Array of all matches.search(): Alias for evaluate() (Symfony-compatible).json_decode() or Laravel’s Http client for API responses.API Response Parsing
public function parseStripeResponse($response)
{
$data = json_decode($response->getBody(), true);
$amount = JsonPath::search($data, '$.data.object.amount');
$currency = JsonPath::search($data, '$.data.object.currency');
return [$amount, $currency];
}
Dynamic Query Building
public function getNestedValue($json, string $path, $default = null)
{
return JsonPath::search($json, $path) ?? $default;
}
// Usage:
$age = $this->getNestedValue($userJson, '$.user.profile.age', 0);
Filtering with Predicates
$expensiveItems = JsonPath::search($inventory, '$.items[?(@.price > 100)].name');
// Returns: ["Laptop", "Monitor"]
Building Paths from User/DB-Supplied Keys
// Since 8.1.9, backslashes and control characters in quoted keys are escaped correctly.
// Use the library's escaping helper instead of hand-rolling quotes:
$path = '$[' . JsonPath::key($columnName) . ']'; // safe for keys containing " \ or \n
$value = $jsonPath->evaluate($path, $payload);
Hand-concatenating "'$columnName'" still breaks on quotes, backslashes, and control characters—especially now that escaping is strict.
Regex Predicates (match() vs search())
// match(): anchored full-string match (RFC 9535)
$isoDates = $jsonPath->evaluateAll($data, '$[?match(@.date, "[0-9]{4}-[0-9]{2}-[0-9]{2}")]');
// search(): substring match anywhere in the string
$authors = $jsonPath->evaluateAll($data, '$[?search(@.author, "Rob")]');
// Slashes in patterns must be escaped; anchoring is now enforced as specified.
$urls = $jsonPath->evaluateAll($data, '$[?search(@.url, "https:\\/\\/example\\.com")]');
If a filter previously returned partial matches via match(), it will now return nothing—see Gotchas.
Laravel Service Container Binding
// app/Providers/AppServiceProvider.php
public function register()
{
$this->app->singleton(JsonPath::class, fn() => new JsonPath());
}
// Usage in controllers:
public function __construct(private JsonPath $jsonPath) {}
Validation Helper
public function validateRequiredFields($json, array $paths)
{
foreach ($paths as $path) {
if (empty(JsonPath::search($json, $path))) {
throw new \InvalidArgumentException("Missing required path: $path");
}
}
}
Middleware for API Responses:
public function handle($request, Closure $next)
{
$response = $next($request);
if ($response->isOk()) {
$data = json_decode($response->getBody(), true);
$response->setData(JsonPath::search($data, '$.transformed.path'));
}
return $response;
}
Eloquent JSON Casting:
use Illuminate\Database\Eloquent\Casts\Attribute;
public function transformedJson(): Attribute
{
return Attribute::make(
get: fn($value) => JsonPath::search($value, '$.user.profile.name'),
);
}
Artisan Commands for Data Extraction:
public function handle()
{
$json = file_get_contents('data.json');
$results = JsonPath::searchAll($json, '$.records[*].id');
$this->line('Extracted IDs: ' . implode(', ', $results));
}
Upgrade Checklist for 8.1.9:
composer update symfony/json-path --with-dependencies
php artisan test --filter=JsonPath
Re-run any test asserting on match() results, nullable array slices ($[0:3], $[*]), or paths built from arbitrary keys.
match() Is Anchored, search() Is Not (fixed in 8.1.9)
match() pattern could behave like a substring search. After upgrading, $[?match(@.id, "abc")] no longer matches "xabcx".search() where you actually wanted a substring test, and wrap partial patterns explicitly:
'$[?match(@.id, ".*abc.*")]' // equivalent to the old (buggy) behaviour
Regex Patterns Need Proper Slash Escaping
/ inside match()/search() patterns must be escaped (\\/), otherwise the expression fails to parse or matches nothing (escaping fixed in 8.1.9, so previously-passing patterns may now be rejected).$jsonPath->evaluateAll($data, '$[?search(@.url, "https:\\/\\/api\\.example\\.com")]');
Index and Slice Selectors Now Preserve null (8.1.9)
$[*], $[0], and $[0:3] previously dropped null array elements. They are now returned.$values = array_values(array_filter(
$jsonPath->evaluateAll($data, '$.items[*]'),
fn ($v) => $v !== null
));
Quoted Keys with Backslashes/Control Characters
$['key'] breaks when the key contains ", \, or a newline/tab. Escaping of these was corrected in JsonPath::key() in 8.1.9.Path Evaluation Quirks
$.store..book (recursive descent) may not work as expected in all JSON structures.evaluateAll() and filter results manually if needed:
$allBooks = JsonPath::searchAll($json, '$.store.*[*].book');
Case Sensitivity in Keys
$.User.Name fails if the JSON uses $.user.name.json_decode($json, true, 512, JSON_BIGINT_AS_STRING).Performance with Deep Nesting
$.a.b.c.d[*].e) can slow down evaluation.evaluateAll() for bulk operations.False Matches with Wildcards
*.book matches any object with a book key, not just top-level objects.$.store.book).Empty Results vs. null
evaluate() returns null for missing paths, while evaluateAll() returns an empty array. Since 8.1.9 a null may also be a legitimate value returned by a selector—indistinguishable from "not found" via evaluate() alone.evaluateAll() + count() when presence matters; reserve evaluate() for single-value lookups.Log Path Evaluations:
try {
$result = JsonPath::search($json, $path);
Log::debug("Path [$path] resolved to: ", ['result' => $result]);
} catch (\Symfony\Component\JsonPath\Exception\JsonPathException $e) {
Log::error("Invalid JSONPath [$path]: " . $e->getMessage());
}
Validate JSON First:
if (json_validate($json)) {
$result = JsonPath::search($json, $path);
} else {
throw new \InvalidArgumentException("Invalid JSON provided");
}
Test Edge Cases (extend these with the 8.1.9 behaviours):
// Test empty JSON
$this->assertNull(JsonPath::search([], '$.nonexistent'));
// Test arrays
$this->assertEquals([1, 2], JsonPath::search([1, 2], '$[*]'));
// Test nested arrays
$this->assertEquals([1, 2], JsonPath::search([['a', 1], ['b', 2]], '$[*][1]'));
// Test null preservation in index/slice selectors (8.1.9)
$this->assertSame([null, 'b'], $jsonPath->evaluateAll(['a' => null, 'b' => 'b'], '$[*]'));
$this->assertSame([null, 'b'], $jsonPath->evaluateAll([null, 'b'], '$[0:2]'));
// Test anchored match()
$this->assertSame([], $jsonPath->evaluateAll(
['xabcx', 'abc'],
'$[?match(@, "abc")]'
));
Bisect Upgrades with a Failing Path Corpus: Keep a fixture file of real production paths and assert their outputs; after bumping to 8.1.9 the diff will surface the anchoring/null-selection changes immediately.
Custom JSONPath Functions
contains()).$filtered = collect($json)->filter(fn($item) => str_contains($item, 'search'));
Laravel Facade
// app/Facades/JsonPath.php
namespace App\Facades;
use Illuminate\Support\Facades\Facade;
class JsonPath extends Facade { protected static function getFacadeAccessor() => 'jsonPath'; }
Register in AppServiceProvider:
$this->app->bind('jsonPath', fn() => new \Symfony\Component\JsonPath\JsonPath());
Usage:
$result = \App\Facades\JsonPath::search($json, '$.path');
Safe Path Builder Helper
class PathBuilder
{
public static function key(string $name): string
{
return '[' . JsonPath::key($name) . ']';
}
}
// $jsonPath->evaluate('$.data' . PathBuilder::key($field), $payload);
Performance Optimization
$cacheKey = md5($json . $path);
return Cache::remember($cacheKey, now()->addMinutes(10), fn() =>
JsonPath::search($json, $path)
);
Integration with Laravel HTTP Client
Http client:
// app/Providers/AppServiceProvider.php
use Illuminate\Support\Facades\Http;
Http::macro('jsonPath', function ($path) {
return JsonPath::search($this->json(), $path);
});
Usage:
$response = Http::get('https://api.example.com/data')->jsonPath('$.data.id');
How can I help you explore Laravel packages today?