Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

FAQ

Frequently asked questions about datalogic-rs.

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 — significantly faster than JS implementations
  • Thread SafetyLogic is Send + Sync; wrap in Arc to share
  • Extended Operators — datetime, string operations, error handling, more
  • Type Safety — full Rust type system benefits
  • WASM Support — same engine in browsers and Node.js
  • Zero unsafe — the crate is built with #![forbid(unsafe_code)]

Is datalogic-rs fully compatible with JSONLogic?

Yes. datalogic-rs passes the complete official JSONLogic test suite. It also includes additional operators that extend the specification.


Rust Usage

Should I use v4 or v5?

Use v5 for new projects. The API is cleaner, the default build does not pull in serde_json, and the arena evaluation path is exposed directly. See the Migration Guide for the move from v4.

v5 is a hard cliff — there is no compatibility shim, so plan a single cutover when upgrading from v4. The repo-root MIGRATION.md has the per-call cookbook.

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)
});

Why are custom operator arguments pre-evaluated in v5?

The pre-evaluated, arena-based design makes custom operators behave like built-ins: the engine recurses, hands you &DataValue<'a> borrows, and you return another arena allocation. This avoids the boundary conversion that the v4 Operator trait paid on every call and removes the need for a separate Evaluator trait.

If you need lazy / short-circuit semantics like and / or, that lives in built-in operators today (none of the v5 short-circuit operators are exposed through the public custom-operator surface).

What’s the difference between eval, eval_str, eval_into, and evaluate?

MethodInputOutputNotes
datalogic_rs::eval_str (and eval / eval_into)R: IntoLogic, D: OwnedInputString (or OwnedDataValue / T)Module-level helper backed by a default engine. Use when you don’t need custom operators or non-default config.
Engine::eval_str (and eval / eval_into)R: IntoLogic, D: OwnedInputString (or OwnedDataValue / T)One-shot through a configured engine. Allocates a fresh arena internally.
Engine::evaluate&Logic, any EvalInput, &Bump&'a DataValue<'a>Hot path. Caller owns the arena, result borrows from it.
Session::eval_str (and eval / eval_into)&Logic, D: EvalInputString (or OwnedDataValue / T)Reuses the session’s arena across calls. Caller calls 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. The Node.js target does not require initialization:

const { evaluate } = require('@goplasmatic/datalogic-wasm');
evaluate('{"==": [1, 1]}', '{}', false);

Why do I need to JSON.stringify my data?

The WASM interface uses string-based communication for maximum compatibility:

const result = evaluate(
  JSON.stringify(logic),
  JSON.stringify(data),
  false
);
const value = JSON.parse(result);

How do I use this with TypeScript?

Types are included in the package:

import init, { evaluate, CompiledRule } from '@goplasmatic/datalogic-wasm';

await init();
const result: string = evaluate('{"==": [1, 1]}', '{}', false);

React UI

Why does the editor need explicit dimensions?

React Flow (the underlying library) requires a container with defined dimensions to calculate node positions and viewport.

<div style={{ height: '500px' }}>
  <DataLogicEditor value={expression} />
</div>

Can I use this with Next.js?

Yes. For the App Router, wrap in 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?

Use the missing or missing_some operators:

{"if": [
    {"missing": ["user.email"]},
    "Email required",
    "Valid"
]}

Or use default values with var:

{"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?

It was removed in v5. Literal scalars and arrays already pass through inline, and templated objects belong in templating mode (Engine::builder().with_templating(true).build(), requires feature = "templating").


Configuration

How do I handle NaN in arithmetic?

Use the NanHandling 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, IgnoreValue, ReturnNull.

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]}) always raises Thrown { type: "NaN" }, whatever the setting. {"/": [10.5, 0]} takes the configured path. See Division by Zero for the full comparison table.


Troubleshooting

“Invalid operator” error

In standard mode, unrecognized keys are treated as errors. Either:

  1. Fix the operator name (operators are case-sensitive)
  2. Register a custom operator on the builder
  3. Enable templating mode (feature = "templating") — Engine::builder().with_templating(true).build()

My template key runs as an operator instead of being emitted

The inverse problem, and quieter: no error, just the wrong result. In templating mode a single-key object is always an operator invocation, so {"type": {"var": "x"}} runs the type operator rather than emitting a type field. Around 60 built-in names are affected, 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 are unaffected. Details in Structured Objects and Troubleshooting.

Performance issues with large expressions

  1. Use Session for repeated calls (arena reuse)
  2. Drop to Engine::evaluate with a caller-managed bumpalo::Bump for the absolute hot path
  3. Use feature = "trace" to see which sub-expressions run and how often (iteration counts; the trace records no timings), and a sampling profiler such as perf or Instruments for timing (see Performance)

WASM initialization fails

Ensure you await init() before calling other functions:

await init();
const result = evaluate(...);

For more troubleshooting, see the Troubleshooting Guide.