Czym jest JSONPath?
JSONPath to język zapytań służący do wyodrębniania wartości z dokumentów JSON. Pierwotnie zaproponowany przez Stefana Goessnera w 2007 roku jako odpowiednik XPath dla XML, JSONPath pozwala pisać wyrażenia takie jak $.store.book[*].author, aby wybrać wszystkie pola autora z tablicy książek bez pisania pętli czy ręcznego kodu przeszukiwania. Język ten został ustandaryzowany w lutym 2024 roku jako RFC 9535, opublikowany przez IETF.
Wyrażenie JSONPath zawsze zaczyna się od $ reprezentującego korzeń dokumentu. Od niego łańcuchowane są selektory: notacja z kropką dla kluczy obiektów, notacja z nawiasami dla indeksów tablic, symbole wieloznaczne dla wszystkich dzieci oraz operator rekurencyjnego przeszukiwania (..) do przeszukiwania każdego poziomu zagnieżdżenia. Wyrażenia filtrujące, takie jak [?(@.price < 10)], umożliwiają wybór elementów na podstawie warunków ocenianych względem ich wartości.
JSONPath jest używany w narzędziach do testowania API takich jak Postman, platformach obserwowalności takich jak Datadog i Splunk, silnikach przepływów pracy takich jak Apache NiFi oraz bibliotekach programistycznych w JavaScript, Python, Go, Java i C#. Testowanie wyrażeń na rzeczywistych danych przed ich osadzeniem w kodzie lub plikach konfiguracyjnych zapobiega cichym błędom, gdy struktura JSON nie odpowiada założeniom.
Dlaczego warto używać tego testera JSONPath?
Pisanie wyrażeń JSONPath ręcznie jest podatne na błędy. Brakująca kropka, zły typ nawiasu lub nieprawidłowa składnia filtra mogą zwracać puste wyniki bez żadnego komunikatu o błędzie. To narzędzie daje natychmiastową wizualną informację zwrotną o tym, co pasuje do Twojego wyrażenia.
Przypadki użycia JSONPath
Informacje o składni JSONPath
RFC 9535 definiuje standardową składnię JSONPath. Poniższa tabela obejmuje operatory, które będziesz używać w większości zapytań. Wszystkie wyrażenia zaczynają się od $ (węzeł główny) i łączą jeden lub więcej selektorów do nawigacji po strukturze dokumentu.
| Operator | Opis | Przykład |
|---|---|---|
| $ | Root element | $.store |
| .key | Child property | $.store.book |
| [n] | Array index (zero-based) | $.store.book[0] |
| [*] | All elements in array/object | $.store.book[*] |
| .. | Recursive descent | $..author |
| [start:end] | Array slice | $.store.book[0:2] |
| [?()] | Filter expression | $.store.book[?(@.price<10)] |
| @ | Current node (inside filter) | $.store.book[?(@.isbn)] |
JSONPath vs jq vs XPath
JSONPath, jq i XPath rozwiązują ten sam problem (zapytania do danych strukturalnych) dla różnych formatów i przypadków użycia. JSONPath celuje w JSON i jest dostępny jako biblioteka w większości języków. jq to samodzielne narzędzie CLI z własnym językiem filtrów Turinga. XPath operuje na XML i jest częścią stosu specyfikacji W3C.
| Funkcja | JSONPath | jq | XPath |
|---|---|---|---|
| Format danych | JSON | JSON | XML |
| Dostęp do pierwszego elementu | $.store.book[0] | .store.book[0] | /store/book[1] |
| Rekurencyjne przeszukiwanie | $..price | .. | .price? | //price |
| Wyrażenie filtrujące | [?(@.price<10)] | select(.price < 10) | [price<10] |
| Specyfikacja | RFC 9535 | stedolan.github.io | W3C XPath 3.1 |
Przykłady kodu
Jak ewaluować wyrażenia JSONPath w popularnych językach i narzędziach. Każdy przykład używa tej samej struktury JSON księgarni dla porównania.
import { JSONPath } from 'jsonpath-plus';
const data = {
store: {
book: [
{ title: 'Moby Dick', price: 8.99 },
{ title: 'The Great Gatsby', price: 12.99 }
]
}
};
// Get all book titles
JSONPath({ path: '$.store.book[*].title', json: data });
// → ['Moby Dick', 'The Great Gatsby']
// Recursive descent — find every price in the document
JSONPath({ path: '$..price', json: data });
// → [8.99, 12.99]
// Filter — books under $10
JSONPath({ path: '$.store.book[?(@.price < 10)]', json: data });
// → [{ title: 'Moby Dick', price: 8.99 }]from jsonpath_ng.ext import parse
import json
data = {
"store": {
"book": [
{"title": "Moby Dick", "price": 8.99},
{"title": "The Great Gatsby", "price": 12.99}
]
}
}
# Get all book titles
expr = parse("$.store.book[*].title")
titles = [match.value for match in expr.find(data)]
# → ['Moby Dick', 'The Great Gatsby']
# Recursive descent — all prices
expr = parse("$..price")
prices = [match.value for match in expr.find(data)]
# → [8.99, 12.99]
# Filter — books under $10
expr = parse("$.store.book[?price < 10]")
cheap = [match.value for match in expr.find(data)]
# → [{"title": "Moby Dick", "price": 8.99}]package main
import (
"fmt"
"github.com/ohler55/ojg/jp"
"github.com/ohler55/ojg/oj"
)
func main() {
src := `{
"store": {
"book": [
{"title": "Moby Dick", "price": 8.99},
{"title": "The Great Gatsby", "price": 12.99}
]
}
}`
obj, _ := oj.ParseString(src)
// Get all book titles
expr, _ := jp.ParseString("$.store.book[*].title")
results := expr.Get(obj)
fmt.Println(results)
// → [Moby Dick The Great Gatsby]
// Recursive descent — all prices
expr2, _ := jp.ParseString("$..price")
fmt.Println(expr2.Get(obj))
// → [8.99 12.99]
}# jq uses its own syntax, not JSONPath, but solves the same problem.
# Mapping common JSONPath patterns to jq:
# $.store.book[*].title → get all titles
echo '{"store":{"book":[{"title":"Moby Dick"},{"title":"Gatsby"}]}}' | \
jq '.store.book[].title'
# → "Moby Dick"
# → "Gatsby"
# $..price → recursive descent for "price" keys
echo '{"a":{"price":1},"b":{"price":2}}' | \
jq '.. | .price? // empty'
# → 1
# → 2
# Filter: books where price < 10
echo '{"store":{"book":[{"title":"A","price":8},{"title":"B","price":12}]}}' | \
jq '.store.book[] | select(.price < 10)'
# → {"title":"A","price":8}