We follow RFC 9535 strictly and test against the JSONPath Compliance Test Suite.
Table of Contents
Add 'json_p3'
to your Gemfile:
gem 'json_p3', '~> 0.2.1'
Or
gem install json_p3
JSON P3 is cryptographically signed. To be sure the gem you install hasn’t been tampered with, add my public key (if you haven’t already) as a trusted certificate:
gem cert --add <(curl -Ls https://raw.githubusercontent.com/jg-rp/ruby-json-p3/refs/heads/main/certs/jgrp.pem)
Followed by:
gem install json_p3 -P MediumSecurity
JSON P3 has no runtime dependencies, so -P HighSecurity
is OK too. See https://guides.rubygems.org/security/ for more information.
require "json_p3"
require "json"
data = JSON.parse <<~JSON
{
"users": [
{
"name": "Sue",
"score": 100
},
{
"name": "Sally",
"score": 84,
"admin": false
},
{
"name": "John",
"score": 86,
"admin": true
},
{
"name": "Jane",
"score": 55
}
],
"moderator": "John"
}
JSON
JSONP3.find("$.users[[email protected] > 85]", data).each do |node|
puts node.value
end
# {"name"=>"Sue", "score"=>100}
# {"name"=>"John", "score"=>86, "admin"=>true}
Or, reading JSON data from a file:
require "json_p3"
require "json"
data = JSON.load_file("/path/to/some.json")
JSONP3.find("$.some.query", data).each do |node|
puts node.value
end
You could read data from a YAML formatted file too, or any data format that can be loaded into hashes and arrays.
require "json_p3"
require "yaml"
data = YAML.load_file("/tmp/some.yaml")
JSONP3.find("$.users[[email protected] > 85]", data).each do |node|
puts node.value
end
- Change log: https://github.com/jg-rp/ruby-json-p3/blob/main/CHANGELOG.md
- RubyGems: https://rubygems.org/gems/json_p3
- Source code: https://github.com/jg-rp/ruby-json-p3
- Issue tracker: https://github.com/jg-rp/ruby-json-p3/issues
- Python JSONPath RFC 9535 - A Python implementation of JSONPath that follows RFC 9535 strictly.
- Python JSONPath - Another Python package implementing JSONPath, but with additional features and customization options.
- JSON P3 - RFC 9535 implemented in TypeScript.
find(query, value) -> Array<JSONPathNode>
Apply JSONPath expression query to JSON-like data value. An array of JSONPathNode instances is returned, one node for each value matched by query. The returned array will be empty if there were no matches.
Each JSONPathNode
has:
- a
value
attribute, which is the JSON-like value associated with the node. - a
location
attribute, which is a nested array of hash/object names and array indices that were required to reach the node's value in the target JSON document. - a
path()
method, which returns the normalized path to the node in the target JSON document.
require "json_p3"
require "json"
data = JSON.parse <<~JSON
{
"users": [
{
"name": "Sue",
"score": 100
},
{
"name": "Sally",
"score": 84,
"admin": false
},
{
"name": "John",
"score": 86,
"admin": true
},
{
"name": "Jane",
"score": 55
}
],
"moderator": "John"
}
JSON
JSONP3.find("$.users[[email protected] > 85]", data).each do |node|
puts "#{node.value} at #{node.path}"
end
# {"name"=>"Sue", "score"=>100} at $['users'][0]
# {"name"=>"John", "score"=>86, "admin"=>true} at $['users'][2]
compile(query) -> JSONPath
Prepare a JSONPath expression for repeated application to different JSON-like data. An instance of JSONPath
has a find(data)
method, which behaves similarly to the module-level find(query, data)
method.
require "json_p3"
require "json"
data = JSON.parse <<~JSON
{
"users": [
{
"name": "Sue",
"score": 100
},
{
"name": "Sally",
"score": 84,
"admin": false
},
{
"name": "John",
"score": 86,
"admin": true
},
{
"name": "Jane",
"score": 55
}
],
"moderator": "John"
}
JSON
path = JSONP3.compile("$.users[[email protected] > 85]")
path.find(data).each do |node|
puts "#{node.value} at #{node.path}"
end
# {"name"=>"Sue", "score"=>100} at $['users'][0]
# {"name"=>"John", "score"=>86, "admin"=>true} at $['users'][2]
The find
and compile
methods described above are convenience methods equivalent to
JSONP3::DEFAULT_ENVIRONMENT.find(query, data)
and
JSONP3::DEFAULT_ENVIRONMENT.compile(query)
You could create your own environment like this:
require "json_p3"
jsonpath = JSONP3::JSONPathEnvironment.new
nodes = jsonpath.find("$.*", { "a" => "b", "c" => "d" })
pp nodes.map(&:value) # ["b", "d"]
To configure an environment with custom filter functions or non-standard selectors, inherit from JSONPathEnvironment
and override some of its constants or the #setup_function_extensions
method.
class MyJSONPathEnvironment < JSONP3::JSONPathEnvironment
# The maximum integer allowed when selecting array items by index.
MAX_INT_INDEX = (2**53) - 1
# The minimum integer allowed when selecting array items by index.
MIN_INT_INDEX = -(2**53) + 1
# The maximum number of arrays and hashes the recursive descent segment will
# traverse before raising a {JSONPathRecursionError}.
MAX_RECURSION_DEPTH = 100
# One of the available implementations of the _name selector_.
#
# - {NameSelector} (the default) will select values from hashes using string keys.
# - {SymbolNameSelector} will select values from hashes using string or symbol keys.
#
# Implement your own name selector by inheriting from {NameSelector} and overriding
# `#resolve`.
NAME_SELECTOR = NameSelector
# An implementation of the _index selector_. The default implementation will
# select values from arrays only. Implement your own by inheriting from
# {IndexSelector} and overriding `#resolve`.
INDEX_SELECTOR = IndexSelector
# Override this function to configure JSONPath function extensions.
# By default, only the standard functions described in RFC 9535 are enabled.
def setup_function_extensions
@function_extensions["length"] = Length.new
@function_extensions["count"] = Count.new
@function_extensions["value"] = Value.new
@function_extensions["match"] = Match.new
@function_extensions["search"] = Search.new
end
JSONPathError
is the base class for all JSONPath exceptions. The following classes inherit from JSONPathError
and will only occur when parsing a JSONPath expression, not when applying a path to some data.
JSONPathSyntaxError
JSONPathTypeError
JSONPathNameError
JSONPathError
implements #detailed_message
. With recent versions of Ruby you should get useful error messages.
JSONP3::JSONPathSyntaxError: unexpected trailing whitespace
-> '$.foo ' 1:5
|
1 | $.foo
| ^ unexpected trailing whitespace
resolve(pointer, value) -> Object
Resolve a JSON Pointer (RFC 6901) against some data using JSONP3.resolve()
.
require "json_p3"
require "json"
data = JSON.parse <<~JSON
{
"users": [
{
"name": "Sue",
"score": 100
},
{
"name": "Sally",
"score": 84,
"admin": false
},
{
"name": "John",
"score": 86,
"admin": true
},
{
"name": "Jane",
"score": 55
}
],
"moderator": "John"
}
JSON
puts JSONP3.resolve("/users/1", data)
# {"name"=>"Sally", "score"=>84, "admin"=>false}
If a pointer can not be resolved, JSONP3::JSONPointer::UNDEFINED
is returned instead. You can use your own default value using the default:
keyword argument.
# continued from above
pp JSONP3.resolve("/no/such/thing", data, default: nil) # nil
apply(ops, value) -> Object
Apply a JSON Patch (RFC 6902) with JSONP3.apply()
. Data is modified in place.
require "json"
require "json_p3"
ops = <<~JSON
[
{ "op": "add", "path": "/some/foo", "value": { "foo": {} } },
{ "op": "add", "path": "/some/foo", "value": { "bar": [] } },
{ "op": "copy", "from": "/some/other", "path": "/some/foo/else" },
{ "op": "add", "path": "/some/foo/bar/-", "value": 1 }
]
JSON
data = { "some" => { "other" => "thing" } }
JSONP3.apply(JSON.parse(ops), data)
pp data
# {"some"=>{"other"=>"thing", "foo"=>{"bar"=>[1], "else"=>"thing"}}}
JSONP3.apply(ops, value)
is a convenience method equivalent to JSONP3::JSONPatch.new(ops).apply(value)
. Use the JSONPatch
constructor when you need to apply the same patch to different data.
As well as passing an array of hashes following RFC 6902 as ops to JSONPatch
, we offer a builder API to construct JSON Patch documents programmatically.
require "json_p3"
data = { "some" => { "other" => "thing" } }
patch = JSONP3::JSONPatch.new
.add("/some/foo", { "foo" => [] })
.add("/some/foo", { "bar" => [] })
.copy("/some/other", "/some/foo/else")
.copy("/some/foo/else", "/some/foo/bar/-")
patch.apply(data)
pp data
# {"some"=>{"other"=>"thing", "foo"=>{"bar"=>["thing"], "else"=>"thing"}}}
Your contributions and questions are always welcome. Feel free to ask questions, report bugs or request features on the issue tracker or on Github Discussions. Pull requests are welcome too.
The JSONPath Compliance Test Suite is included as a git submodule. Clone the JSON P3 git repository and initialize the CTS submodule.
$ git clone [email protected]:jg-rp/ruby-json-p3.git
$ cd ruby-json-p3
$ git submodule update --init
We use Bundler and Rake. Install development dependencies with:
bundle install
Run tests with:
bundle exec rake test
Lint with:
bundle exec rubocop
And type check with:
bundle exec steep
Run one of the benchmarks with:
bundle exec ruby performance/benchmark_ips.rb
Dump profile data with bundle exec ruby performance/profile.rb
, then generate an HTML flame graph with:
bundle exec stackprof --d3-flamegraph .stackprof-cpu-just-compile.dump > flamegraph-cpu-just-compile.html
Print memory usage to the terminal.
bundle exec ruby performance/memory_profile.rb
bundle exec rake release
and bundle exec rake build
will look for gem-private_key.pem
and gem-public_cert.pem
in ~/.gem
.
On macOS Sonoma using MacPorts and rbenv
, LIBYAML_PREFIX=/opt/local/lib
is needed to install TruffleRuby and when executing any bundle
command.