How to use JSONPath with OpenAPI
JSONPath is a query language that can be used to extract data from JSON documents, and it's becoming increasingly useful in the OpenAPI ecosystem.
A few years ago most API designers, developers, and technical writers would have had very little reason to bump into JSONPath, but its starting to get more and more relevant as more tools and standards start relying on it. So what is JSONPath, what is it used for, and how can you get up to speed with using it?
JSONPath is a query language that can be used to extract data from JSON documents, which at first might not sound very exciting, but remember… OpenAPI is just a JSON (or YAML) document, so you can use JSONPath to poke around in OpenAPI and do various things.
You can use JSONPath for OpenAPI Overlays, to patch OpenAPI documents with extra documentation content, code samples, or whatever else.
You can use JSONPath in Spectral to write incredibly advanced linting rules which can power your automated API Style Guides.
You can even use JSONPath in AWS Step Functions.
JSONPath is popping up all over the the place these days, and if you work with OpenAPI it’s definitely a handy tool to have on your belt.
How does JSONPath Work?
JSONPath is one of several query languages which will let you filter, query, and traverse through a chunk of JSON, not just to pull bits out, but to navigate complex data structures, with syntax for getting into specific array indexes, filtering through an objects properties or array values before continuing on to its children.
Here’s a sample JSONPath from the RFC.
$.store.book[?@.price < 10].title
Anyone familiar with XPath in XML will be thinking “hmm, this looks pretty familiar!” and you’re spot on, JSONPath is inspired by XPath. If you’ve never heard of XPath no worries, we’ll start from scratch here.
To see how this works we’ll need some JSON to run it against, so here is an example of some JSON from the RFC.
{
"store": {
"book": [
{
"category": "reference",
"author": "Nigel Rees",
"title": "Sayings of the Century",
"price": 8.95
},
{
"category": "fiction",
"author": "Evelyn Waugh",
"title": "Sword of Honour",
"price": 12.99
},
{
"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",
"isbn": "0-395-19395-8",
"price": 22.99
}
],
"bicycle": {
"color": "red",
"price": 399
}
}
}
Now when you run that through any sort of JSONPath tool, you could expect to see these results.
[
"Sayings of the Century",
"Moby Dick"
]
Syntax #
There is a whole lot of syntax to learn, but once you figure out the constituent pieces you can start to compile them into really advanced queries.
Syntax | Element Description |
---|---|
$ |
root node identifier (Section 2.2) |
@ |
current node identifier (Section 2.3.5) (valid only within filter selectors) |
[<selectors>] |
child segment (Section 2.5.1): selects zero or more children of a node |
.name |
shorthand for [‘name’ ] |
.* |
shorthand for [*] |
..[<selectors>] |
descendant segment (Section 2.5.2): selects zero or more descendants of a node |
..name |
shorthand for .. [’ name’ ] |
..* |
shorthand for ..[*] |
'name' |
name selector (Section 2.3.1): selects a named child of an object |
* |
wildcard selector (Section 2.3.2): selects all children of a node |
3 |
index selector (Section 2.3.3): selects an indexed child of an array (from 0) |
0:100:5 |
array slice selector (Section 2.3.4): start:end:step for arrays |
?<logical-expr> |
filter selector (Section 2.3.5): selects particular children using a logical expression |
length(@.foo) |
function extension (Section 2.4): invokes a function in a filter expression |
Overview of JSONPath Syntax, from RFC 9535.
Examples
If that isn’t making too much sense, here are some examples to help you visualize.
JSONPath | Intended Result |
---|---|
$.store.book[*].author |
the authors of all books in the store |
$..author |
all authors |
$.store.* |
all things in the store, which are some books and a red bicycle |
$.store..price |
the prices of everything in the store |
$..book[2] |
the third book |
$..book[2].author |
the third book’s author |
$..book[2].publisher |
empty result: the third book does not have a “publisher” member |
$..book[-1] |
the last book in order |
$..book[0,1] |
the first two books |
$..book[:2] |
the first two books |
$..book[?@.isbn] |
all books with an ISBN number |
$..book[?@.price<10] |
all books cheaper than 10 |
$..* |
all member values and array elements contained in the input value |
Example JSONPath Expressions and Their Intended Results When Applied to the Example JSON Value, from RFC 9535: 1.5. JSONPath Examples.
By combining these bits of example syntax together you can do amazing and powerful things with JSONPath, so let’s look at how to do those amazing things in OpenAPI.
JSONPath & OpenAPI
Take an OpenAPI document, like the Train Travel API.
git clone github.com/bump-sh-examples/train-travel-api
cd train-travel-api
Then install jsonpath-cli just so we can try some things out.
npm install -g @jsware/jsonpath-cli
Optional, if you’re working with YAML, you might want to convert from YAML to JSON in the CLI too.
brew install yq
yq eval -o=json openapi.yaml > openapi.json
Don’t worry this is just for playing around, all of the tooling that uses JSONPath will support YAML without bodges like this. Let’s just get on the same page for this guide.
Querying OpenAPI with JSONPath
Once you have a JSON file to work with, we can use the jpp
command, pass in a JSON/YAML document, and provide a JSONPath expression to query the document for specific parts.
$ jpp --pretty '$.info' openapi.json
[
{
"title": "Train Travel API",
"description": "API for finding and booking train trips across Europe.",
"version": "1.0.0",
"contact": {
"name": "Train Support",
"url": "https://example.com/support",
"email": "support@example.com"
},
"license": {
"name": "Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International",
"identifier": "CC-BY-NC-SA-4.0"
}
}
]
In this example $
refers to the root JSON document, then .info
is using dot notation to access the info
key in that object.
We can get a bit more advanced, and pull up a list of paths.
$ jpp --pretty '$.paths.*~' openapi.json
[
"/stations",
"/trips",
"/bookings",
"/bookings/{bookingId}",
"/bookings/{bookingId}/payment"
]
This uses the .*
syntax which is basically grabbing all children of the paths object, then using ~
to grab the keys instead of the values.
What sort of query language would JSONPath be if we could not do queries? Let’s pull up a list of paths which are a get
or a post
, but ignore all the put
, patch
, delete
, etc.
$ jpp --pretty '$.paths[?(@.put || @.post)]~' openapi.json
[
"/bookings",
"/bookings/{bookingId}/payment"
]
OpenAPI Overlays powered by JSONPath
One of the main uses for JSONPath will be for working OpenAPI documents, often by technical writers or other folks in the API governance space to check or improve OpenAPI documents.
Overlays are a list of actions, which make up a “target” which is a JSONPath, and an operation of either “update” or “remove”.
Let’s look at an update command.
# overlays.yaml
overlay: 1.0.0
info:
title: Overlay to customise API for Protect Earth
version: 0.0.1
actions:
- target: '$.info'
description: Update description and contact for our audience.
update:
description: >
A new and much more interesting long form description, which has all sorts of
Markdown, or more specifically [CommonMark](https://commonmark.org/) which
is _like_ Markdown but **better**, because it's an actual standard instead of a
series of sometimes vaguely consistent conventions.
Anyway, this is a good place to write all sorts of helpful stuff, link to other
getting started content, link to where people can find access tokens, or even
paste some code samples for getting your first API request off the ground.
contact:
name: Support Team
url: https://example.com/contact
email: support@example.org
This overlays file is pointing to the JSONPath target $.info
, then updating the object with the new bits of OpenAPI for description
and contact
, as per the OpenAPI specification. This can be handy for improving the quality of all sorts of descriptions, not just info, and for popping in support team contact information if the API developers inevitably forgot to mention that sort of thing.
Instead of using those yq
or jpp
tools we grabbed just to practice, we can use the Bump.sh CLI which has support for Overlays built in, and thankfully it’ll work just fine with YAML or JSON.
npm install -g bump-cli
bump overlay openapi.yaml overlays.yaml > openapi.new.yaml
If we were to run that overlay on the Train Travel API, the resulting openapi.new.yaml
would like like this:
openapi: 3.1.0
info:
title: Train Travel API
description: >
A new and much more interesting long form description, which has all sorts
of Markdown, or more specifically [CommonMark](https://commonmark.org/)
which is _like_ Markdown but **better**, because its an actual standard
instead of a series of sometimes vaguely consistent conventions.
Anyway, this is a good place to write all sorts of helpful stuff, link to
other getting started content, link to where people can find access tokens,
or even paste some code samples for getting your first API request off the
ground.
version: 1.0.0
contact:
name: Support Team
url: 'https://example.com/contact'
email: support@example.org
license:
name: Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
identifier: CC-BY-NC-SA-4.0
# snipped
When combined with more advanced queries you can start to get really specific with bits of the OpenAPI document you’d like to update, enabling all sorts of random use cases like cleaning up the servers list for publishing an API Catalogue, removing Development and Staging servers not accessible or relevant to API consumers.
# openapi.yaml
openapi: 3.1.0
servers:
- url: http://localhost:3000
description: Development
- url: https://api-staging.example.com
description: Staging
- url: https://api.example.com
description: Production
An overlay can target the servers array with $.servers
then query through them with $.servers[?(@.description=="Development" || @.description=="Staging")]
, which is looking through objects in the array, and looking through the children for description: Development
or description: Staging
using basically JavaScript syntax.
The Overlay for this would combine that JSONPath target with remove: true
operation like this:
# overlays.yaml
overlay: 1.0.0
info:
title: Overlay to customise API
version: 0.0.1
actions:
- target: '$.servers[?(@.description=="Development" || @.description=="Staging")]'
description: Remove Development and Staging servers but leave anything else.
remove: true
That would leave this resulting OpenAPI.
# openapi.yaml
openapi: 3.1.0
servers:
- url: https://api.example.com
description: Production
Then the Developer Experience folks decide to roll out a Mocking or Sandbox experience, where consumers can play around with requests without actually triggering real emails, real data, or spending real money, but how can we show everyone where that is? Do we have to go and pester all the API teams to add it? Nope, just add another action.
# overlays.yaml
overlay: 1.0.0
info:
title: Overlay to customise API
version: 0.0.1
actions:
- target: '$.servers[?(@.description=="Development" || @.description=="Staging")]'
description: Remove Development and Staging servers but leave anything else.
remove: true
- target: '$.servers'
description: Let everyone know about our amazing new hosted mocking/sandbox server.
update:
- description: Sandbox
url: https://api-sandbox.example.com/
Leaning more about JSONPath
JSONPath made it to IETF “proposed standard” RFC status in 2024 (RFC 9535), but before then it was in a similar position to Markdown in the days before CommonMark, in that there are a few different variations of JSONPath as a concept.
- JSONPath “The Blog Post” - Written by Stefan Gössner in 2007.
- jsonpath.com - An online evaluator which as far as I can tell matches the blog post.
- JSONPath-Plus - A popular (but now abandoned) fork which expands on the original specification to add some additional operators.
- Nimma - A fork of JSONPath Plus created by the Stoplight team for Spectral to handle more advanced use cases. A list of caveats can be found here.
Then to further compound this confusion, all of the implementations have different support for certain features, and have filled in the grey areas differently due to their own interpretations and community requests. The amazing JSONPath Comparison project has collated all of the differences into a massive test suite and published the results, which was really helpful in shaping the new standard. Hopefully this will help tools converge, and we can forget all about this incompatibility.
For now, try to follow the RFC 9535 syntax, and use tooling which lines up with that syntax. Unfortunately that means not using jsonpath.com
, and even the jpp
CLI tool we used earlier is JSONPath Plus, which has a few differences to the RFC…
The Bump.sh CLI overlays
functionality is JSONPath RFC 9535 compliant, and if you spot any valid RFC JSONPath syntax not working as expected please create an issue on GitHub so we can get that sorted out.