FAQ
General
What is JSONLogic?
JSONLogic is a way to write portable, safe logic rules as JSON. The specification is available at jsonlogic.com.
Why use datalogic-rs instead of the reference implementation?
- Performance: about 84x faster than json-logic-js across the shared benchmark suites (see Performance)
- One engine in eight runtimes: Rust, Node.js, browser WASM, Python, Go, JVM, .NET and PHP all run the same core
- Thread safety:
LogicisSend + Sync; wrap it inArcto share it - Extended operators: datetime, string, array and object helpers,
try/throw, flagd feature-flag operators - Rule checking:
checkreports unknown operators and wrong argument counts before a rule runs - No
unsafe: the crate is built with#![forbid(unsafe_code)]
Is datalogic-rs fully compatible with JSONLogic?
It passes the official JSONLogic test suite and adds operators beyond
the specification. A few defaults differ from json-logic-js (an empty
object is falsy, cross-type == raises an error); see
Coming from json-logic-js.
Rust Usage
Should I use v4 or v5?
Use v5 (5.9.0 is current) for new projects. The default build does
not pull in serde_json, and you can evaluate into your own arena. v5
has no compatibility shim for v4 code, so plan a single cutover; the
Migration Guide and the repo-root MIGRATION.md have the
details.
How do I share compiled rules across threads?
Logic is Send + Sync. Wrap it in Arc to share:
use datalogic_rs::Engine;
use std::sync::Arc;
let engine = Arc::new(Engine::new());
let compiled = engine.compile_arc(rule).unwrap();
let compiled_clone = Arc::clone(&compiled);
std::thread::spawn(move || {
let mut session = engine.session();
session.eval_str(&compiled_clone, data)
});
A Session borrows its engine. To store a session in a struct, send it
to another thread, or hold it across an .await, use SharedSession,
which holds the engine by Arc.
How do I find out which data fields a rule reads?
Ask the compiled rule for its facts (facts() in every binding):
use datalogic_rs::Engine;
let engine = Engine::new();
let logic = engine
.compile(r#"{"and": [{">": [{"var": "age"}, 18]}, {"==": [{"var": "country"}, "US"]}]}"#)
.unwrap();
let facts = logic.facts();
let reads: Vec<String> = facts.reads().iter().map(|p| p.segments().join(".")).collect();
assert_eq!(reads, ["age", "country"]);
assert!(facts.reads_complete()); // no computed path, no custom operator
The facts also list the operators the rule uses and whether its result depends only on its data. See Rule Analysis.
Why are custom operator arguments pre-evaluated in v5?
With pre-evaluated arguments, a custom operator works like a built-in:
the engine evaluates the arguments, hands you &DataValue<'a> borrows,
and you return another arena allocation. This drops the boundary
conversion the v4 Operator trait paid on every call and the separate
Evaluator trait.
A custom operator therefore cannot short-circuit. Lazy evaluation belongs
to built-ins such as and, or, if and ??.
What’s the difference between eval, eval_str, eval_into, and evaluate?
| Method | Input | Output | Notes |
|---|---|---|---|
datalogic_rs::eval_str (and eval / eval_into) | R: IntoLogic, D: OwnedInput | String (or OwnedDataValue / T) | Module-level helper backed by a default engine. Use it when you need no custom operators or configuration. |
Engine::eval_str (and eval / eval_into / eval_as) | R: IntoLogic, D: OwnedInput | String (or OwnedDataValue / T) | One-shot through a configured engine, with a fresh arena per call. |
Engine::evaluate | &Logic, any EvalInput, &Bump | &'a DataValue<'a> | Hot path. You own the arena; the result borrows from it. |
Session::eval_str (and eval / eval_into / eval_as) | &Logic, D: EvalInput | String (or OwnedDataValue / T) | Reuses the session’s arena across calls. You call session.reset() between batches. |
Session::eval_borrowed | &Logic, D: EvalInput | &'a DataValue<'a> | Zero-copy result; valid until the next &mut self call. |
The typed eval_into::<T> paths (and the serde_json::Value boundary
on EvalInput / IntoLogic) require feature = "serde_json".
JavaScript / WASM Usage
Do I need to call init() in Node.js?
No. Under Node.js the package resolves to its nodejs build, which
instantiates the module on load:
const { Engine } = require('@goplasmatic/datalogic-wasm');
new Engine().evalStr('{"==": [1, 1]}', '{}'); // "true"
On a Node.js server, the native @goplasmatic/datalogic-node package is
faster and takes JS objects.
Why do I need to JSON.stringify my data?
The WASM binding takes and returns JSON text, so rules, data and results cross the JS/WASM boundary as strings:
const engine = new Engine();
const rule = engine.compile(JSON.stringify(logic));
const value = JSON.parse(rule.evaluate(JSON.stringify(data)));
How do I use this with TypeScript?
The package includes type definitions:
import init, { Engine } from '@goplasmatic/datalogic-wasm';
await init();
const engine = new Engine();
const result: string = engine.evalStr('{"==": [1, 1]}', '{}');
The free evaluate function and the CompiledRule class are deprecated
in 5.8.0 and removed in 6.0; use Engine.
React UI
Why does the editor need explicit dimensions?
The editor draws with React Flow, which needs a container with a defined height to lay out nodes and the viewport.
<div style={{ height: '500px' }}>
<DataLogicEditor value={expression} />
</div>
Can I use this with Next.js?
Yes. With the App Router, render it from a client component:
'use client';
import '@goplasmatic/datalogic-ui/styles.css';
import { DataLogicEditor } from '@goplasmatic/datalogic-ui';
export function Editor({ expression }) {
return <DataLogicEditor value={expression} />;
}
Operators
How do I access array elements by index?
Use the var operator with numeric path segments:
{"var": "items.0.name"}
What’s the difference between == and ===?
==: Loose equality (with type coercion, like JavaScript)===: Strict equality (no type coercion)
{"==": [1, "1"]} // true
{"===": [1, "1"]} // false
How do I handle missing data?
Test for it with missing or missing_some:
{"if": [
{"missing": ["user.email"]},
"Email required",
"Valid"
]}
Or give var a default:
{"var": ["user.email", "no-email@example.com"]}
or also works as a fallback, because it returns the deciding operand
rather than a boolean:
{"or": [{"var": "nickname"}, "Anonymous"]}
// Data: {"nickname": "Ada"} -> "Ada"
// Data: {} -> "Anonymous"
What happened to the preserve operator?
v5 removed it. Literal scalars and arrays pass through inline, and
templated objects belong in templating mode: Engine::compile_template
for one rule, or Engine::builder().with_templating(true).build() for the
engine (both need feature = "templating").
Configuration
How do I handle NaN in arithmetic?
Set NanHandling on the configuration:
use datalogic_rs::{Engine, EvaluationConfig, NanHandling};
let config = EvaluationConfig::default()
.with_arithmetic_nan_handling(NanHandling::IgnoreValue);
let engine = Engine::builder().with_config(config).build();
Options: ThrowError (default), CoerceToZero (substitutes 0), IgnoreValue (skips the value), ReturnNull.
How do I make a misspelled data path fail instead of returning null?
Set MissingVar::Error ("missing_var": "error" in a binding’s config).
A var / val read that finds nothing then raises VariableNotFound
naming the path:
use datalogic_rs::{Engine, EvaluationConfig, MissingVar};
let config = EvaluationConfig::default().with_missing_var(MissingVar::Error);
let engine = Engine::builder().with_config(config).build();
let err = engine
.eval_str(r#"{">": [{"var": "user.agee"}, 18]}"#, r#"{"user": {"age": 30}}"#)
.unwrap_err();
assert_eq!(err.tag(), "VariableNotFound");
A var with a default, a field that is present and null, missing,
missing_some and exists behave as before, and try catches the error.
See Missing Variables.
How do I change division by zero behavior?
use datalogic_rs::{EvaluationConfig, DivisionByZeroHandling};
let config = EvaluationConfig::default()
.with_division_by_zero(DivisionByZeroHandling::ReturnNull);
Options: ReturnSaturated (default, f64::MAX/MIN with the dividend’s
sign), ThrowError, ReturnNull, ReturnInfinity.
The setting applies to the float path only: an integer dividend over an
integer zero ({"/": [10, 0]}) raises Thrown { type: "NaN" } under
every setting. {"/": [10.5, 0]} takes the configured path. See
Division by Zero for the
comparison table.
Troubleshooting
“Invalid operator” error
Outside templating mode, an unknown operator key compiles and raises
InvalidOperator when evaluation reaches it. check finds it before
the rule runs and suggests the operator one edit away (see
Rule Analysis). Then either:
- Fix the operator name (operators are case-sensitive)
- Register a custom operator on the builder
- Compile the rule as a template (
Engine::compile_template,feature = "templating") if the key is meant as an output field
My template key runs as an operator instead of being emitted
This is the inverse problem, and it raises no error, only a wrong result.
In templating mode a single-key object whose key names an operator is an
operator call, so {"type": {"var": "x"}} runs the type operator rather
than emitting a type field. Every built-in name is affected (87 names,
counting the aliases var, ?: and match, in a build with every
operator family), plus any custom operator you registered.
Turn on the key escape and prefix the key:
Engine::builder().with_templating(true).with_template_key_escape('$'),
then write {"$type": ...} to emit type and {"$$type": ...} to emit
a literal $type. It is off by default, so existing templates keep their
meaning. check in template mode also warns about an output key one edit
away from an operator name. Details in
Structured Objects
and Troubleshooting.
Performance issues with large expressions
- Use a
Sessionfor repeated calls, so they reuse one arena - For the hottest path, call
Engine::evaluatewith abumpalo::Bumpyou manage - Use
feature = "trace"to see which sub-expressions run and how often (the trace records iteration counts, not timings), and a sampling profiler such as perf or Instruments for timing (see Performance)
WASM initialization fails
With the web build, await init() before you construct an Engine:
await init();
const engine = new Engine();
For more troubleshooting, see the Troubleshooting Guide.
See Also
- Troubleshooting — solutions for common Rust, WASM, and React UI errors
- Basic Concepts — how JSONLogic rules, truthiness, and data contexts work
- Migration Guide — upgrading across 5.x releases or from v4
- Coming from json-logic-js — differences from the reference JS implementation