Project

jsonata

0.0
The project is in a healthy, maintained state
A Ruby port of jsonata-js, the reference implementation of JSONata - a lightweight query and transformation language for JSON data
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

~> 13.0
~> 3.0
 Project Readme

JSONata-RB

A Ruby port of jsonata-js, the reference implementation of JSONata - a lightweight query and transformation language for JSON data.

This is a faithful, file-by-file port of the JavaScript implementation, and it is tested against the jsonata-js test suite itself (included as a git submodule), so it supports the full JSONata language and function library.

Installation

Add this line to your application's Gemfile:

gem 'jsonata'

And then execute:

bundle install

Usage

require 'jsonata'

data = {
  example: [
    { value: 4 },
    { value: 7 },
    { value: 13 }
  ]
}

# Compile an expression, then evaluate it
expression = Jsonata.expression('$sum(example.value)')
expression.evaluate(data)  # => 24

# Or use the convenience method
Jsonata.evaluate('$sum(example.value)', data)  # => 24

See the JSONata documentation for the language and function library.

Variable bindings

Jsonata.evaluate('value > $threshold', { value: 150 }, { threshold: 100 })  # => true

expr = Jsonata.expression('value > $threshold')
expr.assign('threshold', 100)
expr.evaluate({ value: 150 })  # => true

Custom functions

Any Ruby callable can be bound as a function, optionally with a signature to validate its arguments:

expr = Jsonata.expression('$double(value)')
expr.assign('double', ->(x) { x * 2 })
expr.evaluate({ value: 21 })  # => 42

expr = Jsonata.expression('$add(1, 2)')
expr.register_function('add', ->(a, b) { a + b }, '<nn:n>')
expr.evaluate  # => 3

expr = Jsonata.expression('$greet("World")')
expr.register_function('greet', '<s:s>') { |name| "Hello, #{name}" }
expr.evaluate  # => "Hello, World"

Errors

Syntax and evaluation errors raise Jsonata::Error, which carries the same fields as the error objects thrown by jsonata-js (code, position, token, value, ...):

begin
  Jsonata.evaluate('1 +')
rescue Jsonata::Error => e
  e.code      # => "S0207"
  e.position  # => 3
  e.message   # => "Unexpected end of expression"
end

Options

Jsonata.expression(expr, options) accepts the same options as jsonata-js:

Option Description
:recover Attempt to recover on parse errors (see Expression#errors)
:timeout Evaluation timeout in milliseconds (error D1012)
:stack Maximum evaluation depth (error D1011)
:sequence Maximum sequence length (error D2015)
:regex_engine A callable (source, flags) returning an object that responds to match(str, pos)

Mapping between JSON and Ruby values

  • JSON objects are Hashes. Symbol keys in the input are converted to Strings; results always have String keys.
  • JSON null and JavaScript undefined are both returned as nil. In the input, a nil nested inside a Hash or Array is JSON null, while a top-level nil input means there is no input (undefined). Expression#evaluate_raw returns the internal representation, in which null is Jsonata::NULL and undefined is nil.
  • Numbers are Integers when they are integral (and within the range of a JavaScript safe integer), and Floats otherwise. Arithmetic follows JavaScript (IEEE 754 double) semantics.
  • Functions (lambdas defined in an expression, or built-in functions) are returned as Jsonata::Lambda and Jsonata::NativeFunction objects.

Differences from jsonata-js

  • Evaluation is synchronous (jsonata-js evaluate returns a Promise).
  • Regular expressions use Ruby's regex engine. JavaScript regex syntax is translated where the two differ (e.g. ^/$ anchors), but some exotic constructs may behave differently.
  • Strings are indexed by Unicode code point, so $match indexes differ from jsonata-js (which uses UTF-16 code units) for strings containing characters outside the Basic Multilingual Plane.

Development

The jsonata-js repository is included as a git submodule at vendor/jsonata-js, so clone with:

git clone --recurse-submodules <repo-url>
# or, in an existing clone
git submodule update --init

Then:

bundle install
bundle exec rspec                         # run all tests
GROUP=hof-map,regex bundle exec rspec     # run specific test-suite groups
bundle exec rake console                  # interactive console
bundle exec rubocop                       # lint

Structure of the port

Each Ruby file is a port of the corresponding jsonata-js source file, keeping the same structure, function names (in snake_case), and comments, so that upstream changes can be applied to the Ruby code directly.

jsonata-js jsonata-rb
src/jsonata.js lib/jsonata/evaluator.rb
src/parser.js lib/jsonata/parser.rb
src/functions.js lib/jsonata/functions.rb
src/datetime.js lib/jsonata/datetime.rb
src/signature.js lib/jsonata/signature.rb
src/utils.js lib/jsonata/utils.rb (plus helpers emulating JavaScript)
test/run-test-suite.js spec/test_suite_spec.rb

Tracking jsonata-js

The tests come from the jsonata-js test suite (vendor/jsonata-js/test/test-suite). To pick up jsonata-js changes:

bundle exec rake jsonata_js:update  # move the submodule to the latest jsonata-js master, and list the new commits
bundle exec rake jsonata_js:diff    # show the src/ and test/ changes to port
bundle exec rspec                   # port the src/ changes until the tests pass
git add vendor/jsonata-js lib       # commit the submodule bump together with the ported changes

The jsonata-js sync workflow (.github/workflows/jsonata-js-sync.yaml) does this daily, one jsonata-js commit at a time: for each new jsonata-js master commit, it bumps the submodule to it, follows a version change in package.json, has Claude port any src/ and test/ changes, and commits. It opens (or updates) a pull request from the jsonata-js-sync branch, which it rebases onto master on every run, so fixes pushed to the branch are kept. If a commit cannot be ported cleanly, it stops there and makes the PR a draft; nothing more is synced until the PR is fixed and marked ready for review. Merge the PR with "Rebase and merge" (not squash).

The jsonata-js tags workflow (.github/workflows/jsonata-js-tags.yaml) then gives the commit that syncs a jsonata-js release tag the same tag (if its version matches), including tags that jsonata-js adds to older commits later. It does not release the gem.

The steps of both are in bin/jsonata-js-sync. With DRY_RUN=1, bin/jsonata-js-sync sync commits to a local jsonata-js-sync branch and prints the PR description, and bin/jsonata-js-sync tag tags locally, without pushing.

Versioning

For consistency, the gem's version follows the jsonata-js version it is ported from, so jsonata 2.2.2 implements the same language and passes the same test suite as jsonata-js 2.2.2. Releases that contain only Ruby-specific changes, between upstream releases, add a fourth segment (e.g. 2.2.2.1).

License

The gem is available as open source under the terms of the MIT License.

Acknowledgments

This is a Ruby port of the JSONata JavaScript library created by Andrew Coleman and contributors.