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:
| Feature | Operators |
|---|---|
ext-string | length, starts_with, ends_with, upper, lower, trim, split |
ext-array | sort, slice, group_by, distinct |
ext-math | abs, ceil, floor |
ext-control | exists, ??, switch (alias match), type |
ext-object | keys, values, entries |
error-handling | try, throw |
datetime | datetime, timestamp, parse_date, format_date, date_diff, now |
all-operators | every 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 write | You 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:
- Exactly one prefix is stripped from every key, not only from keys that
collide with an operator.
$totalis not an operator name and is still stripped. So a template that emits genuinely$-prefixed keys — MongoDB’s$setand$oid, JSON Schema’s$schemaand$ref— must double them.Engine::check_workflowreportsESCAPED_TEMPLATE_KEYfor every escaped key, which is how you find them all when upgrading. - It applies at every depth, including inside a
mapbody or anifbranch — anywhere a template key appears. - Two keys may not collapse to the same name.
{"$a": 1, "a": 2}would emitatwice, soEngine::buildrefuses 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 defaultcargo add dataflow-rsbuild has none of them, so an expression usinglengthorswitchcan work in the playground and be silently inert in your Rust service.
Best Practices
- Use var Defaults - Provide defaults for optional fields
- Check Existence - Use
!!to verify field exists before use - Keep It Simple - Complex logic may be better in custom functions
- Test Expressions - Use the playground to test JSONLogic before deploying