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

JSONLogic

Dataflow-rs uses JSONLogic for conditions and data transformations.

Overview

JSONLogic is a way to write rules as JSON. It’s used in dataflow-rs for:

  • Rule Conditions - Control when rules (workflows) execute, evaluated against the full context (data, metadata, temp_data)
  • Action Conditions - Control when actions (tasks) execute
  • Map Function - Transform and copy data

Basic Syntax

JSONLogic operations are objects with a single key (the operator) and value (the arguments):

{"operator": [argument1, argument2, ...]}

Data Access

var - Access Data

// Access top-level field
{"var": "data.name"}

// Access nested field
{"var": "data.user.profile.email"}

// Access array element
{"var": "data.items.0"}

// Default value if missing
{"var": ["data.optional", "default value"]}

Context Structure

In dataflow-rs, the context available to JSONLogic is:

{
    "data": { ... },
    "metadata": { ... },
    "temp_data": { ... }
}

Access fields with:

{"var": "data.field"}
{"var": "metadata.type"}
{"var": "temp_data.intermediate"}

Values the engine must never record — signing keys, partner tokens — are deliberately not in this tree. They live in an engine-scoped store and are read with the reserved operator {"secret": "name"}; see Secrets for where that is allowed and what it guarantees.

Comparison Operators

Equality

{"==": [{"var": "data.status"}, "active"]}
{"===": [{"var": "data.count"}, 0]}  // Strict equality
{"!=": [{"var": "data.status"}, "deleted"]}
{"!==": [{"var": "data.count"}, null]}  // Strict inequality

Numeric Comparisons

{">": [{"var": "data.age"}, 18]}
{">=": [{"var": "data.score"}, 60]}
{"<": [{"var": "data.price"}, 100]}
{"<=": [{"var": "data.quantity"}, 10]}

Between

{"<=": [1, {"var": "data.x"}, 10]}  // 1 <= x <= 10
{"<": [1, {"var": "data.x"}, 10]}   // 1 < x < 10

Boolean Logic

and, or, not

{"and": [
    {">=": [{"var": "data.age"}, 18]},
    {"==": [{"var": "data.verified"}, true]}
]}

{"or": [
    {"==": [{"var": "data.status"}, "active"]},
    {"==": [{"var": "data.status"}, "pending"]}
]}

{"!": {"var": "data.disabled"}}

Truthy/Falsy

// Check if value is truthy (not null, false, 0, "")
{"!!": {"var": "data.email"}}

// Check if value is falsy
{"!": {"var": "data.deleted"}}

Conditionals

if-then-else

{"if": [
    {">=": [{"var": "data.score"}, 90]}, "A",
    {">=": [{"var": "data.score"}, 80]}, "B",
    {">=": [{"var": "data.score"}, 70]}, "C",
    "F"
]}

Ternary

{"if": [
    {"var": "data.premium"},
    "VIP Customer",
    "Standard Customer"
]}

String Operations

cat - Concatenation

{"cat": ["Hello, ", {"var": "data.name"}, "!"]}

substr - Substring

{"substr": [{"var": "data.text"}, 0, 10]}  // First 10 characters
{"substr": [{"var": "data.text"}, -5]}     // Last 5 characters

in - Contains

// Check if substring exists
{"in": ["@", {"var": "data.email"}]}

// Check if value in array
{"in": [{"var": "data.status"}, ["active", "pending"]]}

Numeric Operations

Arithmetic

{"+": [{"var": "data.a"}, {"var": "data.b"}]}
{"-": [{"var": "data.total"}, {"var": "data.discount"}]}
{"*": [{"var": "data.price"}, {"var": "data.quantity"}]}
{"/": [{"var": "data.total"}, {"var": "data.count"}]}
{"%": [{"var": "data.n"}, 2]}  // Modulo

Min/Max

{"min": [{"var": "data.a"}, {"var": "data.b"}, 100]}
{"max": [{"var": "data.x"}, 0]}  // Ensure non-negative

Array Operations

merge - Combine Arrays

{"merge": [
    {"var": "data.list1"},
    {"var": "data.list2"}
]}

map - Transform Array

{"map": [
    {"var": "data.items"},
    {"*": [{"var": ""}, 2]}  // Double each item
]}

filter - Filter Array

{"filter": [
    {"var": "data.items"},
    {">=": [{"var": ""}, 10]}  // Items >= 10
]}

reduce - Aggregate Array

{"reduce": [
    {"var": "data.items"},
    {"+": [{"var": "accumulator"}, {"var": "current"}]},
    0  // Initial value
]}

all/some/none

{"all": [{"var": "data.items"}, {">=": [{"var": ""}, 0]}]}
{"some": [{"var": "data.items"}, {"==": [{"var": ""}, "special"]}]}
{"none": [{"var": "data.items"}, {"<": [{"var": ""}, 0]}]}

Try It

Want more features? Try the Full Debugger UI with step-by-step execution and workflow visualization.

Common Patterns

Safe Field Access

// Default to empty string
{"var": ["data.optional", ""]}

// Check existence first
{"if": [
    {"!!": {"var": "data.optional"}},
    {"var": "data.optional"},
    "default"
]}

Null Coalescing

{"if": [
    {"!!": {"var": "data.primary"}},
    {"var": "data.primary"},
    {"var": "data.fallback"}
]}

Type Checking

The operator is type, not typeof, and it requires the ext-control operator family. It returns "null", "boolean", "number", "string", "array" or "object".

// Check if string
{"===": [{"type": {"var": "data.field"}}, "string"]}

// Check if array
{"===": [{"type": {"var": "data.items"}}, "array"]}

With the datetime family also enabled, type classifies date-shaped strings as "datetime" or "duration" rather than "string".

Operator Families (Cargo Features)

Everything documented above this section is core JSONLogic and is always available. The remaining operators ship behind cargo features, all off by default:

FeatureOperators
ext-stringlength, starts_with, ends_with, upper, lower, trim, split
ext-arraysort, slice, group_by, distinct
ext-mathabs, ceil, floor
ext-controlexists, ??, switch (alias match), type
ext-objectkeys, values, entries
error-handlingtry, throw
datetimedatetime, timestamp, parse_date, format_date, date_diff, now
all-operatorsevery family above
[dependencies]
dataflow-rs = { version = "3.12", features = ["ext-string", "ext-control"] }

error-handling names the JSONLogic try/throw operators. It has nothing to do with dataflow-rs’s own error handling, which is always on.

Three placements are easy to get wrong: length is in ext-string, not ext-array, even though it counts array elements as well as string characters; type is in ext-control, not a family of its own; and the split between the last two array-flavoured families follows the input, not the output — entries is in ext-object even though it produces an array (of {key, value} rows), while group_by and distinct are in ext-array even though they are natural companions to it. Iterating an object’s entries with group_by therefore needs both families enabled.

Enabling a family can change existing rules

dataflow-rs runs JSONLogic in templating mode, where an unrecognised operator name is not an error — the object passes through as literal data. That is what makes these features non-additive.

Before ext-string, a mapping that produces {"length": {"var": "data.x"}} stores that object verbatim. After ext-string, the same mapping stores a number. Audit your rules for object keys matching any operator in the table above before enabling its family.

The fix is to say which you meant. {"$length": …} is a literal object with a length field, whatever families are enabled — see Literal keys and the $ escape below.

datetime goes further and changes core operators. With it on, ==, <, <=, > and >= first try to parse plain string operands as datetimes or durations:

{"==": ["2024-01-15T00:00:00Z", "2024-01-15T01:00:00+01:00"]}

This is false without datetime and true with it — the two strings are different bytes naming the same instant.

Literal keys and the $ escape

Templating mode makes every single-key object an operator invocation. So {"cat": ["a", "b"]} is the cat operator and evaluates to "ab" — there was, until 3.9, no way to write an object with a field genuinely called cat.

Prefixing a key with $ says “this is data, not a call”:

You writeYou get
{"cat": ["a", "b"]}"ab" — the operator
{"$cat": ["a", "b"]}{"cat": ["a", "b"]} — the object
{"$$oid": "abc"}{"$oid": "abc"}
{"$total": 1}{"total": 1}

Three things to know:

  1. Exactly one prefix is stripped from every key, not only from keys that collide with an operator. $total is not an operator name and is still stripped. So a template that emits genuinely $-prefixed keys — MongoDB’s $set and $oid, JSON Schema’s $schema and $ref — must double them. Engine::check_workflow reports ESCAPED_TEMPLATE_KEY for every escaped key, which is how you find them all when upgrading.
  2. It applies at every depth, including inside a map body or an if branch — anywhere a template key appears.
  3. Two keys may not collapse to the same name. {"$a": 1, "a": 2} would emit a twice, so Engine::build refuses it (DUPLICATE_TEMPLATE_KEY).

The prefix is $ on every build and fixed for the life of an engine, but an authoring tool that renders or validates the spelling should read it rather than hardcode it:

#![allow(unused)]
fn main() {
use dataflow_rs::Engine;

let engine = Engine::builder().build().unwrap();
assert_eq!(engine.template_key_escape(), '$');
}

It is the companion to Engine::operator_names below: that answers which names are live, this answers how to opt a key out of being one.

A multi-key object is always an output template, so its keys need no escape unless one of them starts with $:

{"amount": {"var": "data.total"}, "currency": "EUR"}

The same is true of a single-key object whose key names no operator: {"result": {"var": "data.x"}} evaluates its argument and emits {"result": …}. Escaping is only needed when the key is an operator name, or when the key really starts with $.

Checking what a build evaluates

Because a disabled operator is inert rather than an error, “does this expression do anything?” is not a question you can answer by reading the rule alone. Engine::operator_names reports the exact vocabulary of the running engine — core, plus whichever families were compiled in, plus anything registered through EngineBuilder::with_datalogic_operator:

#![allow(unused)]
fn main() {
use dataflow_rs::Engine;
fn _demo() -> dataflow_rs::Result<()> {
let engine = Engine::builder().build()?;
let names: Vec<&str> = engine.operator_names().collect();

assert!(names.contains(&"var"));      // core: always present
// `length` appears here only when the `ext-string` feature is enabled.
Ok(()) }
}

That is the check an authoring tool should run before telling someone their expression is fine.

Note for JavaScript users: the WASM package is built with all-operators, so every family in the table is live in the browser and in the Playground. A default cargo add dataflow-rs build has none of them, so an expression using length or switch can work in the playground and be silently inert in your Rust service.

Best Practices

  1. Use var Defaults - Provide defaults for optional fields
  2. Check Existence - Use !! to verify field exists before use
  3. Keep It Simple - Complex logic may be better in custom functions
  4. Test Expressions - Use the playground to test JSONLogic before deploying