jsonlogic-hs: JsonLogic rule engine, checked against the JavaScript reference

[ data, json, library, mit ] [ Propose Tags ] [ Report a vulnerability ]

jsonlogic-hs is a native Haskell implementation of JsonLogic (https://jsonlogic.com), a small, closed rule language that is itself JSON, so rules can be stored, shared between systems, and evaluated against data without running arbitrary code.

It follows the JavaScript reference implementation, json-logic-js 2.0.5: all 278 cases of the classic shared suite pass, and in lenient arity mode it gives the reference's own answers on a committed catalogue of about 27,000 deliberately awkward rules (type coercion, UTF-16 strings, signed zero, NaN, infinities, undefined, numeric edge cases, odd arities, and every rule of the community suite), with a 140,000-case tier and thousands of random rules checked against the reference in CI. The few deliberate differences are listed in docs/COMPATIBILITY.md.

Rules are compiled once and run many times. Evaluation is pure and bounded: rule depth, evaluation steps, string length and array length all have configurable limits, so rules from an untrusted source cannot run unbounded work or exhaust memory. Every error carries the JSON Pointer of the rule node that failed. Arity checking is strict by default and can be switched to the reference's lenient behaviour. Custom operators are supplied by the caller, and dataUse reports which parts of the data a rule can read.

The supported API is the JsonLogic module; docs/TUTORIAL.md walks through it. The JsonLogic.Core modules are exposed for advanced use and may change before 1.0.


[Skip to Readme]

Flags

Manual Flags

NameDescriptionDefault
werror

Treat warnings as errors (enable in dev/CI, off for releases).

Disabled
differential

Build the differential-test executable (needs Node.js to run).

Disabled
bench

Build the benchmark executable.

Disabled

Use -f <flag> to enable a flag, or -f -<flag> to disable that flag. More info

Downloads

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees

Candidates

  • No Candidates
Versions [RSS] 0.1.0.0
Change log CHANGELOG.md
Dependencies aeson (>=2.1 && <2.4), base (>=4.18 && <4.23), bytestring (>=0.11 && <0.13), containers (>=0.6 && <0.9), data-fix (>=0.3 && <0.4), directory (>=1.3 && <1.4), jsonlogic-hs, process (>=1.6 && <1.7), QuickCheck (>=2.14 && <2.20), scientific (>=0.3 && <0.4), tasty-bench (>=0.3 && <0.5), text (>=2.0 && <2.3), vector (>=0.12.3 && <0.14) [details]
Tested with ghc ==9.6.7 || ==9.8.4 || ==9.10.3 || ==9.12.4 || ==9.14.1
License MIT
Copyright (c) 2026 Sabrinathan Nair
Author Sabrinathan Nair
Maintainer 259497327+Bombay-Boyz@users.noreply.github.com
Uploaded by bombayboyz at 2026-10-23T10:17:40Z
Category Data, JSON
Home page https://github.com/Bombay-Boyz/jsonlogic-hs
Bug tracker https://github.com/Bombay-Boyz/jsonlogic-hs/issues
Source repo head: git clone https://github.com/Bombay-Boyz/jsonlogic-hs.git
Distributions
Executables jsonlogic-hs-bench, jsonlogic-hs-differential
Downloads 3 total (3 in the last 30 days)
Rating (no votes yet) [estimated by Bayesian average]
Your Rating
  • λ
  • λ
  • λ
Status Docs uploaded by user
Build status unknown [no reports yet]

Readme for jsonlogic-hs-0.1.0.0

[back to package description]

jsonlogic-hs

A native Haskell implementation of JsonLogic, the small rule language that is itself JSON, so rules can be stored, shared between systems and evaluated against data without running arbitrary code.

A substantial amount of the code and documentation was written with AI assistance.

It is held to the JavaScript reference implementation, json-logic-js 2.0.5. On our own 26,980-case catalogue of reference answers, the next closest implementation we tested scores 84.8% (see How it compares).

Author: Sabrinathan Nair. Licence: MIT LICENSE.

Classic shared suite (jsonlogic.com/tests.json) 278 / 278
Outlier catalogue, answers recorded from json-logic-js 26,980 / 26,980 (lenient mode)
Random differential test against json-logic-js every CI run, fresh seed
Community suite (json-logic/compat-tables) 782 / 1138 lenient, 839 / 1138 strict
Haddock coverage 100% of every module

The community suite (json-logic/compat-tables) is mostly written for the newer JSON Logic 2.0 dialect, which this library does not implement. We run it for reference, not as a target. In lenient mode the library scores exactly what json-logic-js scores on it, file for file (782 / 1138), which is what you would expect from a faithful copy of that reference. Libraries that implement the 2.0 dialect score much higher on it; see How it compares.

Quick start

{-# LANGUAGE OverloadedStrings #-}

import Data.Aeson (decode)
import JsonLogic (apply)

main :: IO ()
main = print (apply <$> decode "{\"==\": [{\"var\": \"a\"}, 1]}" <*> decode "{\"a\": 1}")
-- Just (Right (Bool True))

apply rule data takes both as aeson Values and returns Either Error Value. docs/TUTORIAL.md walks through everything else: compiling once, configuring limits, custom operators, reading errors, and finding out what a rule reads.

The API in one screen

apply           :: Value -> Value -> Either Error Value
applyWith       :: Operators -> Value -> Value -> Either Error Value
applyWithConfig :: Config -> Operators -> Value -> Value -> Either Error Value

compile :: Config -> Operators -> Value -> Either Error Rule   -- read, check, build once
run     :: Rule -> Value -> Either Error Value                 -- run many times
dataUse :: Rule -> DataUse                                     -- what it can read

operatorsFromList :: [(OperatorName, CustomOperator)] -> Either OperatorsError Operators
type CustomOperator = Value -> [Value] -> Either Text Value

data Error = ParseFailure Location ParseError | EvalFailure Location EvalError
renderLocation :: Location -> Text                             -- an RFC 6901 JSON Pointer

Everything is pure: no IO, no global state. The supported API is the JsonLogic module; JsonLogic.Core.* is exposed for advanced use and may change before 1.0 (docs/STABILITY.md).

What you can rely on

  • The reference's answers. Mixed-type behaviour is JavaScript's: "1" == 1, [] == 0, "10" < "9", NaN, the infinities, negative zero, undefined (what an empty and yields, which is not null), UTF-16 string length, indexing and ordering, ECMAScript whitespace in numeric strings, and numbers printed digit for digit as JavaScript prints them. Every place it deliberately differs is listed with a reason and a test in docs/COMPATIBILITY.md.
  • Bounded work. The default Config limits rule depth (64), evaluation steps (1,000,000), string length (1 MiB in UTF-16 code units) and array length (100,000), so a hostile rule cannot hang the process or exhaust its memory. Each limit is exact at both edges (docs/LIMITS.md).
  • Errors you can act on. A closed set of constructors to match on, each with the JSON Pointer of the node that failed: renderError gives at /if/1: unknown operator "nope".
  • Strict or lenient arity. By default a wrong argument count is a precise ArityMismatch (the reference silently pads and ignores). With arityMode = Lenient the library behaves exactly like the reference.
  • Custom operators supplied by the caller, refused if they would shadow a builtin, resolved once when the rule is compiled, and held to the same limits.
  • Speed. Interpretation is in the same range as json-logic-js running in V8 on the same machine (map over 10,000 elements: about 2.5 ms here, 2.1 ms for the reference), with compile once and run many.

How it is checked

Every claim above has a mechanical check behind it, and the checks are checked: scripts/check-harness.sh feeds every runner missing, empty, truncated and tampered fixtures and demands that each fails.

  • The classic suite, pinned at exactly 278 cases.
  • The outlier catalogue (test/outliers.jsonl): rules and data chosen to be awkward (type coercion, every whitespace code point, 700-digit numbers, shortest-digit ties, signed zero, surrogate pairs, odd arities of every operator, deep and wide rules, every community rule), with the answers json-logic-js gave. Generated deterministically by differential/outliers.js; CI fails if the committed file is stale. Every case runs in both arity modes. A 140,000-case tier runs nightly.
  • A differential test that generates random rules, compares each answer with json-logic-js running in Node, and shrinks any disagreement to a minimal reproduction.
  • The community suite, scored per file and pinned in test/community/expected.json, so the score only changes on purpose.
  • Unit and property tests: boundary triples for every limit, amplification rules that must stop quickly, algebraic laws, error locations that always resolve to the operator at fault, the fast numeric algorithms against exact ones on hundreds of thousands of doubles, and dataUse's guarantee.
  • Hygiene, blocking in CI: -Werror on the primary GHC, a matrix of GHC 9.6 to 9.14 plus macOS and Windows, hlint, fourmolu, shellcheck, actionlint, cabal check, a build of the source distribution on its own, and a downstream project that uses only the public module. Dead code is checked with weeder, which is advisory for now.

Implementing JsonLogic in another language? The fixtures are plain JSON and JSONL with a documented format; docs/CONFORMANCE.md explains how to run them against your implementation.

How it compares

We ran seven other JsonLogic implementations against the same fixtures on 2026-10-23. The full report is in docs/COMPARISON.md. The runners and raw results are in harness/.

This is a fidelity test, not a neutral correctness benchmark. The outlier catalogue records what json-logic-js 2.0.5 answered, so json-logic-js scores 100% by design. The catalogue was generated by this project's author. Implementations that follow the newer JSON Logic 2.0 dialect are scored against behaviour they do not aim for.

Results

Implementation Classic suite (278) Outlier catalogue (26,980) Community suite (1,138)
jsonlogic-hs (measured by this repo's own test runner, see note below) 278 (100%) 26,980 (100%, lenient mode) 782 lenient / 839 strict
json-logic-js 2.0.5 (the reference) 278 (100%) 26,980 (100%) 782 (68.7%)
jsonlogic 0.5.1 (Rust) 270 (97.1%) 22,881 (84.8%) 761 (66.9%)
panzi-json-logic 1.0.1 (Python) 277 (99.6%) 21,361 (79.2%) 790 (69.4%)
json-logic-engine 5.0.7 (JS, interpreted) 278 (100%) 16,475 (61.1%) 1,119 (98.3%)
json-logic-engine 5.0.7 (JS, compiled) 278 (100%) 16,400 (60.8%) 1,119 (98.3%)
json-logic-qubit 0.9.1 (Python) 277 (99.6%) 15,117 (56.0%) 759 (66.7%)
datalogic-rs 5.4.0 (Rust, default features) 278 (100%) 14,989 (55.6%) 1,060 (93.1%)
datalogic-rs 5.4.0 (Rust, all extensions) 278 (100%) 14,955 (55.4%) 1,105 (97.1%)
jsonlogic-rs 0.5.0 (Rust) 276 (99.3%) 13,801 (51.2%) 782 (68.7%)

Note on the jsonlogic-hs row. Its figures come from this repository's own test runners (cabal test all), which use the same fixtures and the judging rules in docs/CONFORMANCE.md. The other rows were produced by the comparison harness in harness/. jsonlogic-hs itself has not been run through that harness, so its row did not go through exactly the same judge as the others.

What this shows

  • On the outlier catalogue, the closest other implementation we tested scores 84.8%.
  • That implementation, the jsonlogic Rust crate, panics on 108 cases and cannot read 73 rules that contain numbers such as 1e309.
  • The next closest scores 79.2% (panzi-json-logic).
  • On the community suite, json-logic-engine and datalogic-rs score much higher than we do (93-98%).
  • They score higher because they implement the JSON Logic 2.0 dialect, and jsonlogic-hs does not.
  • The two columns pull in opposite directions: following the 2.0 dialect lowers the outlier score.

What this does not show

  • jsonlogic-hs was not run in the comparison harness, because the machine had no suitable GHC. Its row uses the results of this repository's own test runners, not the harness judge the other rows went through.
  • The older Haskell jsonlogic package (0.1.0.0) was not tested.
  • Speed was not compared.
  • datalogic-rs was tested at 5.4.0, not the latest release (5.9.0), because newer versions need a newer Rust than was available.
  • Two published scores could not be reproduced exactly: the jsonlogic crate (761 here, 798 published) and datalogic-rs (1,105 here, 1,138 published).
  • json-logic-engine has configurable limits on depth and size. We did not test whether they hold, so bounded evaluation is not unique to this library.

What it is not

  • Not the JSON Logic 2.0 dialect (val, ??, exists, try/throw, NaN as an error). That dialect is what most of the community suite tests, so a low score there reflects a different target, not a bug. It is not implemented.
  • Not a sandbox for custom operators. The library bounds its own work and a custom operator's result, not the operator's own computation.
  • Not a wall-clock timeout. Limits bound work; add System.Timeout.timeout if seconds matter.

Building and testing

cabal build
cabal test all            # unit/property, classic suite, outliers, community score

The repository's cabal.project turns on what released builds do not: -Werror, the differential tester (needs Node.js; see differential/README.md) and the benchmarks.

Command What it checks
bash scripts/lint.sh hlint, fourmolu, fixture hashes, shellcheck
bash scripts/weeder.sh dead code
bash scripts/check-harness.sh the test runners fail when they should; the outlier fixture is current
bash scripts/verify-sdist.sh the package as Hackage would receive it
bash scripts/consumer-smoke.sh a separate project using only JsonLogic
bash scripts/bench.sh benchmarks, with their environment (bench/README.md)

Further reading

Licence

MIT. See LICENSE. The vendored test suites keep their own licences: test/community/ is Apache-2.0 (its LICENSE is included); test/tests.json is the shared classic suite from jsonlogic.com.