packages feed

jsonlogic-hs-0.1.0.0: README.md

# jsonlogic-hs

A native Haskell implementation of [JsonLogic](https://jsonlogic.com), 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](https://github.com/jwadhams/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](#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](#how-it-compares).

## Quick start

```haskell
{-# 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 `Value`s and returns
`Either Error Value`. [`docs/TUTORIAL.md`](https://github.com/Bombay-Boyz/jsonlogic-hs/blob/main/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

```haskell
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`](https://github.com/Bombay-Boyz/jsonlogic-hs/blob/main/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`](https://github.com/Bombay-Boyz/jsonlogic-hs/blob/main/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`](https://github.com/Bombay-Boyz/jsonlogic-hs/blob/main/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`](https://github.com/Bombay-Boyz/jsonlogic-hs/blob/main/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`](https://github.com/Bombay-Boyz/jsonlogic-hs/blob/main/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`](https://github.com/Bombay-Boyz/jsonlogic-hs/blob/main/docs/COMPARISON.md).
The runners and raw results are in [`harness/`](https://github.com/Bombay-Boyz/jsonlogic-hs/tree/main/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`](https://github.com/Bombay-Boyz/jsonlogic-hs/blob/main/docs/CONFORMANCE.md). The other rows were
produced by the comparison harness in [`harness/`](https://github.com/Bombay-Boyz/jsonlogic-hs/tree/main/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`](https://github.com/Bombay-Boyz/jsonlogic-hs/blob/main/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`](https://github.com/Bombay-Boyz/jsonlogic-hs/blob/main/bench/README.md)) |

## Further reading

- [`docs/TUTORIAL.md`](https://github.com/Bombay-Boyz/jsonlogic-hs/blob/main/docs/TUTORIAL.md): using the library, step by step.
- [`docs/ARCHITECTURE.md`](https://github.com/Bombay-Boyz/jsonlogic-hs/blob/main/docs/ARCHITECTURE.md): how it is built, and why.
- [`docs/COMPATIBILITY.md`](https://github.com/Bombay-Boyz/jsonlogic-hs/blob/main/docs/COMPATIBILITY.md): every difference from the reference.
- [`docs/COMPARISON.md`](https://github.com/Bombay-Boyz/jsonlogic-hs/blob/main/docs/COMPARISON.md): how other JsonLogic implementations score on these fixtures, and the limits of that comparison.
- [`docs/LIMITS.md`](https://github.com/Bombay-Boyz/jsonlogic-hs/blob/main/docs/LIMITS.md): resource limits and what a step is.
- [`docs/CONFORMANCE.md`](https://github.com/Bombay-Boyz/jsonlogic-hs/blob/main/docs/CONFORMANCE.md): using these fixtures to test any implementation.
- [`docs/STABILITY.md`](https://github.com/Bombay-Boyz/jsonlogic-hs/blob/main/docs/STABILITY.md): what is covered by the versioning policy.
- [`docs/AUDIT-2026-10.md`](https://github.com/Bombay-Boyz/jsonlogic-hs/blob/main/docs/AUDIT-2026-10.md): the external audit's findings and how each was resolved.
- [`docs/adr/`](https://github.com/Bombay-Boyz/jsonlogic-hs/tree/main/docs/adr/): recorded decisions and deviations from the engineering standard.
- [`VISION.md`](https://github.com/Bombay-Boyz/jsonlogic-hs/blob/main/VISION.md) and [`ENGINEERING_STANDARD.md`](https://github.com/Bombay-Boyz/jsonlogic-hs/blob/main/ENGINEERING_STANDARD.md): the design and the rules this codebase follows.

## Licence

MIT. See [`LICENSE`](https://github.com/Bombay-Boyz/jsonlogic-hs/blob/main/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.