Try the JSONLogic Online Debugger to interactively test your rules
datalogic-rs is a high-performance Rust implementation of JSONLogic for evaluating logical rules expressed as JSON. It provides a fast, memory-efficient, and thread-safe way to evaluate complex business rules, feature flags, dynamic pricing logic, and more.
Why datalogic-rs?
- Fast: Uses OpCode-based dispatch with compile-time optimization for maximum performance
- Thread-Safe: Compile once, evaluate anywhere with zero-copy
Arcsharing - Intuitive: Works seamlessly with
serde_json::Value - Extensible: Add custom operators with a simple trait
- Feature-Rich: 59 built-in operators including datetime, regex, and error handling
- Fully Compliant: Passes the official JSONLogic test suite
How It Works
datalogic-rs uses a two-phase approach:
-
Compilation: Your JSON logic is parsed and compiled into an optimized
CompiledLogicstructure. This phase:- Assigns OpCodes to built-in operators for fast dispatch
- Pre-evaluates constant expressions
- Analyzes structure for templating mode
-
Evaluation: The compiled logic is evaluated against your data with:
- Direct OpCode dispatch (no string lookups at runtime)
- Context stack for nested operations (map, filter, reduce)
- Efficient value passing with minimal allocations
Quick Example
#![allow(unused)]
fn main() {
use datalogic_rs::DataLogic;
use serde_json::json;
let engine = DataLogic::new();
// Define a rule: is the user's age greater than 18?
let rule = json!({ ">": [{ "var": "age" }, 18] });
// Compile once
let compiled = engine.compile(&rule).unwrap();
// Evaluate against different data
let result = engine.evaluate_owned(&compiled, json!({ "age": 21 })).unwrap();
assert_eq!(result, json!(true));
let result = engine.evaluate_owned(&compiled, json!({ "age": 16 })).unwrap();
assert_eq!(result, json!(false));
}
What is JSONLogic?
JSONLogic is a standard for expressing logic rules as JSON. This makes it:
- Portable: Rules can be stored in databases, sent over APIs, or embedded in configuration
- Language-agnostic: The same rules work across different implementations
- Human-readable: Rules are easier to understand than arbitrary code
- Safe: Rules can be evaluated without arbitrary code execution
A JSONLogic rule is a JSON object where:
- The key is the operator name
- The value is an array of arguments
{"operator": [arg1, arg2, ...]}
For example:
{"and": [
{">": [{"var": "age"}, 18]},
{"==": [{"var": "country"}, "US"]}
]}
This rule checks if age > 18 AND country == "US".
Next Steps
- Installation - Add datalogic-rs to your project
- Quick Start - Get up and running in minutes
- Operators - Explore all 59 built-in operators
Playground
Want the full experience? Try the Full-Page Visual Editor with examples and resizable panels.
Try JSONLogic expressions right in your browser! This playground uses the visual debugger component powered by WebAssembly.
How to Use
- Logic: Enter your JSONLogic expression in the Logic panel
- Data: Enter the JSON data to evaluate against in the Data panel
- Diagram: View the visual diagram of your logic expression
- Examples: Use the dropdown to load pre-built examples
Quick Reference
Basic Operators
| Operator | Example | Description |
|---|---|---|
var | {"var": "x"} | Access variable |
== | {"==": [1, 1]} | Equality |
>, <, >=, <= | {">": [5, 3]} | Comparison |
and, or | {"and": [true, true]} | Logical |
if | {"if": [cond, then, else]} | Conditional |
+, -, *, / | {"+": [1, 2]} | Arithmetic |
Array Operations
| Operator | Example | Description |
|---|---|---|
map | {"map": [arr, expr]} | Transform elements |
filter | {"filter": [arr, cond]} | Filter elements |
reduce | {"reduce": [arr, expr, init]} | Reduce to value |
all, some, none | {"all": [arr, cond]} | Check conditions |
String Operations
| Operator | Example | Description |
|---|---|---|
cat | {"cat": ["a", "b"]} | Concatenate |
substr | {"substr": ["hello", 0, 2]} | Substring |
in | {"in": ["@", "a@b.com"]} | Contains |
Example: Feature Flag
Determine if a user has access to a premium feature:
{
"and": [
{"==": [{"var": "user.plan"}, "premium"]},
{">=": [{"var": "user.accountAge"}, 30]}
]
}
Data:
{
"user": {
"plan": "premium",
"accountAge": 45
}
}
Example: Dynamic Pricing
Calculate a discounted price based on quantity:
{
"if": [
{">=": [{"var": "quantity"}, 100]},
{"*": [{"var": "price"}, 0.8]},
{"if": [
{">=": [{"var": "quantity"}, 50]},
{"*": [{"var": "price"}, 0.9]},
{"var": "price"}
]}
]
}
Data:
{
"quantity": 75,
"price": 100
}
Learn More
- Operators Overview - Full operator documentation
- Getting Started - Using the library
- Use Cases - Real-world examples
Installation
Adding to Your Project
Add datalogic-rs to your Cargo.toml:
[dependencies]
datalogic-rs = "4.0"
serde_json = "1.0"
Or use cargo add:
cargo add datalogic-rs serde_json
Version Selection
- v4.x (recommended): Ergonomic API with
serde_json::Value, simpler to use - v3.x: Arena-based allocation for maximum raw performance
Both versions are actively maintained. Choose v4 for ease of use, v3 if you need every bit of performance.
Feature Flags
datalogic-rs has minimal dependencies by default. All features are included in the base crate.
WebAssembly Support
For WebAssembly targets, use the npm package:
npm install @goplasmatic/datalogic
Or build from source:
cd wasm
wasm-pack build --target web
Minimum Rust Version
datalogic-rs requires Rust 1.70 or later.
Verifying Installation
Create a simple test:
use datalogic_rs::DataLogic;
use serde_json::json;
fn main() {
let engine = DataLogic::new();
let rule = json!({ "+": [1, 2] });
let compiled = engine.compile(&rule).unwrap();
let result = engine.evaluate_owned(&compiled, json!({})).unwrap();
println!("1 + 2 = {}", result);
assert_eq!(result, json!(3));
}
Run with:
cargo run
You should see: 1 + 2 = 3
Quick Start
This guide will get you evaluating JSONLogic rules in minutes.
Basic Workflow
The typical workflow with datalogic-rs is:
- Create an engine instance
- Compile your rule (once)
- Evaluate against data (many times)
#![allow(unused)]
fn main() {
use datalogic_rs::DataLogic;
use serde_json::json;
// 1. Create engine
let engine = DataLogic::new();
// 2. Compile rule
let rule = json!({ ">": [{ "var": "score" }, 50] });
let compiled = engine.compile(&rule).unwrap();
// 3. Evaluate against data
let result = engine.evaluate_owned(&compiled, json!({ "score": 75 })).unwrap();
assert_eq!(result, json!(true));
}
Quick JSON Evaluation
For one-off evaluations, use evaluate_json:
#![allow(unused)]
fn main() {
use datalogic_rs::DataLogic;
use serde_json::json;
let engine = DataLogic::new();
let result = engine.evaluate_json(
r#"{ "+": [1, 2, 3] }"#,
r#"{}"#
).unwrap();
assert_eq!(result, json!(6));
}
Working with Variables
Access data using the var operator:
#![allow(unused)]
fn main() {
use datalogic_rs::DataLogic;
use serde_json::json;
let engine = DataLogic::new();
// Simple variable access
let rule = json!({ "var": "name" });
let compiled = engine.compile(&rule).unwrap();
let result = engine.evaluate_owned(&compiled, json!({ "name": "Alice" })).unwrap();
assert_eq!(result, json!("Alice"));
// Nested variable access with dot notation
let rule = json!({ "var": "user.address.city" });
let compiled = engine.compile(&rule).unwrap();
let data = json!({
"user": {
"address": {
"city": "New York"
}
}
});
let result = engine.evaluate_owned(&compiled, data).unwrap();
assert_eq!(result, json!("New York"));
// Default values
let rule = json!({ "var": ["missing_key", "default_value"] });
let compiled = engine.compile(&rule).unwrap();
let result = engine.evaluate_owned(&compiled, json!({})).unwrap();
assert_eq!(result, json!("default_value"));
}
Conditional Logic
Use if for branching:
#![allow(unused)]
fn main() {
use datalogic_rs::DataLogic;
use serde_json::json;
let engine = DataLogic::new();
let rule = json!({
"if": [
{ ">=": [{ "var": "age" }, 18] },
"adult",
"minor"
]
});
let compiled = engine.compile(&rule).unwrap();
let result = engine.evaluate_owned(&compiled, json!({ "age": 25 })).unwrap();
assert_eq!(result, json!("adult"));
let result = engine.evaluate_owned(&compiled, json!({ "age": 15 })).unwrap();
assert_eq!(result, json!("minor"));
}
Combining Conditions
Use and and or to combine conditions:
#![allow(unused)]
fn main() {
use datalogic_rs::DataLogic;
use serde_json::json;
let engine = DataLogic::new();
// AND: all conditions must be true
let rule = json!({
"and": [
{ ">=": [{ "var": "age" }, 18] },
{ "==": [{ "var": "verified" }, true] }
]
});
let compiled = engine.compile(&rule).unwrap();
let result = engine.evaluate_owned(&compiled, json!({
"age": 21,
"verified": true
})).unwrap();
assert_eq!(result, json!(true));
// OR: at least one condition must be true
let rule = json!({
"or": [
{ "==": [{ "var": "role" }, "admin"] },
{ "==": [{ "var": "role" }, "moderator"] }
]
});
let compiled = engine.compile(&rule).unwrap();
let result = engine.evaluate_owned(&compiled, json!({ "role": "admin" })).unwrap();
assert_eq!(result, json!(true));
}
Array Operations
Filter, map, and reduce arrays:
#![allow(unused)]
fn main() {
use datalogic_rs::DataLogic;
use serde_json::json;
let engine = DataLogic::new();
// Filter: keep elements matching a condition
let rule = json!({
"filter": [
{ "var": "numbers" },
{ ">": [{ "var": "" }, 5] }
]
});
let compiled = engine.compile(&rule).unwrap();
let result = engine.evaluate_owned(&compiled, json!({
"numbers": [1, 3, 5, 7, 9]
})).unwrap();
assert_eq!(result, json!([7, 9]));
// Map: transform each element
let rule = json!({
"map": [
{ "var": "numbers" },
{ "*": [{ "var": "" }, 2] }
]
});
let compiled = engine.compile(&rule).unwrap();
let result = engine.evaluate_owned(&compiled, json!({
"numbers": [1, 2, 3]
})).unwrap();
assert_eq!(result, json!([2, 4, 6]));
}
Error Handling
Handle errors gracefully:
#![allow(unused)]
fn main() {
use datalogic_rs::DataLogic;
use serde_json::json;
let engine = DataLogic::new();
// Compilation errors
let result = engine.compile(&json!({ "unknown_op": [] }));
// Result depends on preserve_structure setting
// Evaluation errors with try/catch
let rule = json!({
"try": [
{ "/": [10, { "var": "divisor" }] },
0 // Fallback value
]
});
let compiled = engine.compile(&rule).unwrap();
let result = engine.evaluate_owned(&compiled, json!({ "divisor": 0 })).unwrap();
assert_eq!(result, json!(0)); // Division by zero caught
}
Next Steps
- Basic Concepts - Understand the architecture
- Operators - Explore all available operators
- Custom Operators - Extend with your own logic
Basic Concepts
Understanding how datalogic-rs works will help you use it effectively.
JSONLogic Format
A JSONLogic rule is a JSON object where:
- The key is the operator name
- The value is an array of arguments (or a single argument)
{ "operator": [arg1, arg2, ...] }
Arguments can be:
- Literal values:
1,"hello",true,null - Arrays:
[1, 2, 3] - Nested operations:
{ "var": "x" }
Examples
// Simple comparison
{ ">": [5, 3] } // true
// Variable access
{ "var": "user.name" } // Access user.name from data
// Nested operations
{ "+": [{ "var": "a" }, { "var": "b" }] } // Add two variables
// Multiple arguments
{ "and": [true, true, false] } // false
Compilation vs Evaluation
datalogic-rs separates rule processing into two phases:
Compilation Phase
When you call engine.compile(&rule), the library:
- Parses the JSON into an internal representation
- Assigns OpCodes to operators for fast dispatch
- Pre-evaluates constant expressions
- Wraps the result in
Arcfor thread-safe sharing
#![allow(unused)]
fn main() {
// Compile once
let compiled = engine.compile(&rule).unwrap();
// The compiled rule can be shared across threads
let compiled_clone = Arc::clone(&compiled);
}
Evaluation Phase
When you call engine.evaluate_owned(&compiled, data):
- Dispatches operations via OpCode (O(1) lookup)
- Accesses data through the context stack
- Returns the result as a
Value
#![allow(unused)]
fn main() {
// Evaluate many times with different data
let result1 = engine.evaluate_owned(&compiled, json!({ "x": 1 })).unwrap();
let result2 = engine.evaluate_owned(&compiled, json!({ "x": 2 })).unwrap();
}
The DataLogic Engine
The DataLogic struct is your main entry point:
#![allow(unused)]
fn main() {
use datalogic_rs::DataLogic;
// Default engine
let engine = DataLogic::new();
// Engine with configuration
let engine = DataLogic::with_config(config);
// Engine with structure preservation (templating mode)
let engine = DataLogic::with_preserve_structure();
// Engine with both
let engine = DataLogic::with_config_and_structure(config, true);
}
The engine:
- Stores custom operators
- Holds evaluation configuration
- Provides compile and evaluate methods
Context Stack
The context stack manages variable scope during evaluation. This is important for array operations like map, filter, and reduce.
#![allow(unused)]
fn main() {
// In a filter operation, "" refers to the current element
let rule = json!({
"filter": [
[1, 2, 3, 4, 5],
{ ">": [{ "var": "" }, 3] } // "" = current element
]
});
// Result: [4, 5]
}
During array operations:
""orvarwith empty string refers to the current element- The outer data context is still accessible
- Nested operations create nested contexts
Evaluation Methods
evaluate_owned
Takes ownership of the data, best for most use cases:
#![allow(unused)]
fn main() {
let result = engine.evaluate_owned(&compiled, json!({ "x": 1 })).unwrap();
}
evaluate
Borrows the data, useful when you need to reuse the data:
#![allow(unused)]
fn main() {
let data = json!({ "x": 1 });
let result = engine.evaluate(&compiled, &data).unwrap();
// data is still available here
}
evaluate_json
Convenience method that parses JSON strings:
#![allow(unused)]
fn main() {
let result = engine.evaluate_json(
r#"{ "+": [1, 2] }"#, // Rule as JSON string
r#"{"x": 10}"# // Data as JSON string
).unwrap();
}
Type Coercion
JSONLogic operators often perform type coercion:
Arithmetic
- Strings are parsed as numbers when possible
"5" + 3=8- Non-numeric strings may result in errors or NaN (configurable)
Comparison
==performs loose equality (type coercion)===performs strict equality (no coercion)
Truthiness
By default, uses JavaScript-style truthiness:
- Falsy:
false,0,"",null,[] - Truthy: everything else
This is configurable via EvaluationConfig.
Thread Safety
CompiledLogic is wrapped in Arc and is Send + Sync:
#![allow(unused)]
fn main() {
use std::sync::Arc;
use std::thread;
let engine = Arc::new(DataLogic::new());
let compiled = engine.compile(&rule).unwrap();
let handles: Vec<_> = (0..4).map(|i| {
let engine = Arc::clone(&engine);
let compiled = Arc::clone(&compiled);
thread::spawn(move || {
engine.evaluate_owned(&compiled, json!({ "x": i }))
})
}).collect();
for handle in handles {
let result = handle.join().unwrap();
// Each thread gets its result
}
}
Next Steps
- Operators Overview - Learn about all available operators
- Configuration - Customize evaluation behavior
- Custom Operators - Extend with your own logic
Operators Overview
datalogic-rs provides 59 built-in operators organized into logical categories. This section documents each operator with syntax, examples, and notes on behavior.
Operator Categories
| Category | Operators | Description |
|---|---|---|
| Variable Access | var, val, exists | Access and check data |
| Comparison | ==, ===, !=, !==, >, >=, <, <= | Compare values |
| Logical | !, !!, and, or | Boolean logic |
| Arithmetic | +, -, *, /, %, max, min, abs, ceil, floor | Math operations |
| Control Flow | if, ?:, ?? | Conditional branching |
| String | cat, substr, in, length, starts_with, ends_with, upper, lower, trim, split | String manipulation |
| Array | merge, filter, map, reduce, all, some, none, sort, slice | Array operations |
| DateTime | datetime, timestamp, parse_date, format_date, date_diff, now | Date and time |
| Missing Values | missing, missing_some | Check for missing data |
| Error Handling | try, throw | Exception handling |
Operator Syntax
All operators follow the JSONLogic format:
{ "operator": [arg1, arg2, ...] }
Some operators accept a single argument without an array:
{ "var": "name" }
// Equivalent to:
{ "var": ["name"] }
Lazy Evaluation
Several operators use lazy (short-circuit) evaluation:
and: Stops at first falsy valueor: Stops at first truthy valueif: Only evaluates the matching branch?:: Only evaluates the matching branch??: Only evaluates fallback if first value is null
This is important when operations have side effects or when you want to avoid errors:
{
"and": [
{ "var": "user" },
{ "var": "user.profile.name" }
]
}
If user is null, the second condition is never evaluated, avoiding an error.
Type Coercion
Operators handle types differently:
Loose vs Strict
==and!=perform type coercion===and!==require exact type match
{ "==": [1, "1"] } // true (loose)
{ "===": [1, "1"] } // false (strict)
Numeric Coercion
Arithmetic operators attempt to convert values to numbers:
{ "+": ["5", 3] } // 8 (string "5" becomes number 5)
Truthiness
Boolean operators use configurable truthiness rules. By default (JavaScript-style):
- Falsy:
false,0,"",null,[] - Truthy: Everything else
Custom Operators
You can add your own operators. See Custom Operators for details.
#![allow(unused)]
fn main() {
engine.add_operator("myop".to_string(), Box::new(MyOperator));
}
Custom operators follow the same syntax:
{ "myop": [arg1, arg2] }
Variable Access Operators
These operators access data from the evaluation context.
var
Access a value from the data object using dot notation.
Syntax:
{ "var": "path" }
{ "var": ["path", default] }
Arguments:
path- Dot-separated path to the value (string)default- Optional default value if path doesn’t exist
Returns: The value at the path, or the default value, or null.
Examples:
// Simple access
{ "var": "name" }
// Data: { "name": "Alice" }
// Result: "Alice"
// Nested access
{ "var": "user.address.city" }
// Data: { "user": { "address": { "city": "NYC" } } }
// Result: "NYC"
// Array index access
{ "var": "items.0" }
// Data: { "items": ["a", "b", "c"] }
// Result: "a"
// Default value
{ "var": ["missing", "default"] }
// Data: {}
// Result: "default"
// Access entire data object
{ "var": "" }
// Data: { "x": 1, "y": 2 }
// Result: { "x": 1, "y": 2 }
Try it:
Notes:
- Empty string
""returns the entire data context - In array operations (
map,filter,reduce),""refers to the current element - Numeric indices work for both arrays and string characters
- Returns
nullif path doesn’t exist and no default is provided
val
Alternative variable access with additional path navigation capabilities.
Syntax:
{ "val": "path" }
{ "val": ["path", default] }
Arguments:
path- Path to the value, supports additional navigation syntaxdefault- Optional default value
Returns: The value at the path, or the default value, or null.
Examples:
// Simple access (same as var)
{ "val": "name" }
// Data: { "name": "Bob" }
// Result: "Bob"
// Nested access
{ "val": "config.settings.enabled" }
// Data: { "config": { "settings": { "enabled": true } } }
// Result: true
Try it:
Notes:
- Similar to
varbut with extended path syntax support - Useful for complex data navigation scenarios
exists
Check if a variable path exists in the data.
Syntax:
{ "exists": "path" }
{ "exists": { "var": "path" } }
Arguments:
path- Path to check (string or var operation)
Returns: true if the path exists, false otherwise.
Examples:
// Check if key exists
{ "exists": "name" }
// Data: { "name": "Alice" }
// Result: true
// Check missing key
{ "exists": "age" }
// Data: { "name": "Alice" }
// Result: false
// Check nested path
{ "exists": "user.profile" }
// Data: { "user": { "profile": { "name": "Bob" } } }
// Result: true
// Check with var
{ "exists": { "var": "fieldName" } }
// Data: { "fieldName": "name", "name": "Alice" }
// Result: true (checks if "name" exists)
Try it:
Notes:
- Returns
falsefor paths that don’t exist - Does not check if the value is null/empty, only if the path exists
- Useful for conditional logic based on data structure
Comparison Operators
Operators for comparing values. All comparison operators support lazy evaluation.
== (Equals)
Loose equality comparison with type coercion.
Syntax:
{ "==": [a, b] }
Arguments:
a- First valueb- Second value
Returns: true if values are equal (after type coercion), false otherwise.
Examples:
// Same type
{ "==": [1, 1] }
// Result: true
// Type coercion
{ "==": [1, "1"] }
// Result: true
{ "==": [0, false] }
// Result: true
{ "==": ["", false] }
// Result: true
// Null comparison
{ "==": [null, null] }
// Result: true
// Arrays
{ "==": [[1, 2], [1, 2]] }
// Result: true
Try it:
Notes:
- Performs type coercion similar to JavaScript’s
== - For strict comparison without coercion, use
===
=== (Strict Equals)
Strict equality comparison without type coercion.
Syntax:
{ "===": [a, b] }
Arguments:
a- First valueb- Second value
Returns: true if values are equal and same type, false otherwise.
Examples:
// Same type and value
{ "===": [1, 1] }
// Result: true
// Different types
{ "===": [1, "1"] }
// Result: false
{ "===": [0, false] }
// Result: false
// Null
{ "===": [null, null] }
// Result: true
Try it:
!= (Not Equals)
Loose inequality comparison with type coercion.
Syntax:
{ "!=": [a, b] }
Arguments:
a- First valueb- Second value
Returns: true if values are not equal (after type coercion), false otherwise.
Examples:
{ "!=": [1, 2] }
// Result: true
{ "!=": [1, "1"] }
// Result: false (type coercion makes them equal)
{ "!=": ["hello", "world"] }
// Result: true
Try it:
!== (Strict Not Equals)
Strict inequality comparison without type coercion.
Syntax:
{ "!==": [a, b] }
Arguments:
a- First valueb- Second value
Returns: true if values are not equal or different types, false otherwise.
Examples:
{ "!==": [1, "1"] }
// Result: true (different types)
{ "!==": [1, 1] }
// Result: false
{ "!==": [1, 2] }
// Result: true
> (Greater Than)
Check if the first value is greater than the second.
Syntax:
{ ">": [a, b] }
{ ">": [a, b, c] }
Arguments:
a,b- Values to comparec- Optional third value for chained comparison
Returns: true if a > b (and b > c if provided), false otherwise.
Examples:
// Simple comparison
{ ">": [5, 3] }
// Result: true
{ ">": [3, 5] }
// Result: false
// Chained comparison (a > b > c)
{ ">": [5, 3, 1] }
// Result: true (5 > 3 AND 3 > 1)
{ ">": [5, 3, 4] }
// Result: false (3 is not > 4)
// String comparison
{ ">": ["b", "a"] }
// Result: true (lexicographic)
// With variables
{ ">": [{ "var": "age" }, 18] }
// Data: { "age": 21 }
// Result: true
Try it:
>= (Greater Than or Equal)
Check if the first value is greater than or equal to the second.
Syntax:
{ ">=": [a, b] }
{ ">=": [a, b, c] }
Arguments:
a,b- Values to comparec- Optional third value for chained comparison
Returns: true if a >= b (and b >= c if provided), false otherwise.
Examples:
{ ">=": [5, 5] }
// Result: true
{ ">=": [5, 3] }
// Result: true
{ ">=": [3, 5] }
// Result: false
// Chained
{ ">=": [5, 3, 3] }
// Result: true (5 >= 3 AND 3 >= 3)
< (Less Than)
Check if the first value is less than the second.
Syntax:
{ "<": [a, b] }
{ "<": [a, b, c] }
Arguments:
a,b- Values to comparec- Optional third value for chained comparison
Returns: true if a < b (and b < c if provided), false otherwise.
Examples:
{ "<": [3, 5] }
// Result: true
{ "<": [5, 3] }
// Result: false
// Chained (useful for range checks)
{ "<": [1, 5, 10] }
// Result: true (1 < 5 AND 5 < 10)
// Range check: is x between 1 and 10?
{ "<": [1, { "var": "x" }, 10] }
// Data: { "x": 5 }
// Result: true
Try it:
<= (Less Than or Equal)
Check if the first value is less than or equal to the second.
Syntax:
{ "<=": [a, b] }
{ "<=": [a, b, c] }
Arguments:
a,b- Values to comparec- Optional third value for chained comparison
Returns: true if a <= b (and b <= c if provided), false otherwise.
Examples:
{ "<=": [3, 5] }
// Result: true
{ "<=": [5, 5] }
// Result: true
{ "<=": [5, 3] }
// Result: false
// Range check (inclusive)
{ "<=": [1, { "var": "x" }, 10] }
// Data: { "x": 10 }
// Result: true (1 <= 10 AND 10 <= 10)
Notes:
- Chained comparisons are useful for range checks
{ "<": [a, x, b] }is equivalent toa < x AND x < b
Logical Operators
Boolean logic operators with short-circuit evaluation.
! (Not)
Logical NOT - negates a boolean value.
Syntax:
{ "!": value }
{ "!": [value] }
Arguments:
value- Value to negate
Returns: true if value is falsy, false if value is truthy.
Examples:
{ "!": true }
// Result: false
{ "!": false }
// Result: true
{ "!": 0 }
// Result: true (0 is falsy)
{ "!": 1 }
// Result: false (1 is truthy)
{ "!": "" }
// Result: true (empty string is falsy)
{ "!": "hello" }
// Result: false (non-empty string is truthy)
{ "!": null }
// Result: true (null is falsy)
{ "!": [] }
// Result: true (empty array is falsy)
{ "!": [1, 2] }
// Note: This negates the array [1, 2], not [value]
// Result: false (non-empty array is truthy)
Try it:
Notes:
- Uses configurable truthiness rules (default: JavaScript-style)
- Falsy values:
false,0,"",null,[] - Truthy values: everything else
!! (Double Not / Boolean Cast)
Convert a value to its boolean equivalent.
Syntax:
{ "!!": value }
{ "!!": [value] }
Arguments:
value- Value to convert to boolean
Returns: true if value is truthy, false if value is falsy.
Examples:
{ "!!": true }
// Result: true
{ "!!": false }
// Result: false
{ "!!": 1 }
// Result: true
{ "!!": 0 }
// Result: false
{ "!!": "hello" }
// Result: true
{ "!!": "" }
// Result: false
{ "!!": [1, 2, 3] }
// Result: true
{ "!!": [] }
// Result: false
{ "!!": null }
// Result: false
Try it:
Notes:
- Equivalent to
{ "!": { "!": value } } - Useful for ensuring a boolean result from any value
and
Logical AND with short-circuit evaluation.
Syntax:
{ "and": [a, b, ...] }
Arguments:
a,b, … - Two or more values to AND together
Returns: The first falsy value encountered, or the last value if all are truthy.
Examples:
// All truthy
{ "and": [true, true] }
// Result: true
// One falsy
{ "and": [true, false] }
// Result: false
// Short-circuit: returns first falsy
{ "and": [true, 0, "never evaluated"] }
// Result: 0
// All truthy returns last value
{ "and": [1, 2, 3] }
// Result: 3
// Multiple conditions
{ "and": [
{ ">": [{ "var": "age" }, 18] },
{ "==": [{ "var": "verified" }, true] },
{ "!=": [{ "var": "banned" }, true] }
]}
// Data: { "age": 21, "verified": true, "banned": false }
// Result: true
Try it:
Notes:
- Short-circuits: stops at first falsy value
- Returns the actual value, not necessarily a boolean
- Empty
andreturnstrue(vacuous truth)
or
Logical OR with short-circuit evaluation.
Syntax:
{ "or": [a, b, ...] }
Arguments:
a,b, … - Two or more values to OR together
Returns: The first truthy value encountered, or the last value if all are falsy.
Examples:
// One truthy
{ "or": [false, true] }
// Result: true
// All falsy
{ "or": [false, false] }
// Result: false
// Short-circuit: returns first truthy
{ "or": [0, "", "found it", "not evaluated"] }
// Result: "found it"
// All falsy returns last value
{ "or": [false, 0, ""] }
// Result: ""
// Default value pattern
{ "or": [{ "var": "nickname" }, { "var": "name" }, "Anonymous"] }
// Data: { "name": "Alice" }
// Result: "Alice" (nickname is null/missing, so returns name)
// Role check
{ "or": [
{ "==": [{ "var": "role" }, "admin"] },
{ "==": [{ "var": "role" }, "moderator"] }
]}
// Data: { "role": "admin" }
// Result: true
Try it:
Notes:
- Short-circuits: stops at first truthy value
- Returns the actual value, not necessarily a boolean
- Useful for default value patterns
- Empty
orreturnsfalse
Truthiness Reference
The default JavaScript-style truthiness:
| Value | Truthy? |
|---|---|
true | Yes |
false | No |
1, 2, -1, 3.14 | Yes |
0, 0.0 | No |
"hello", "0", "false" | Yes |
"" | No |
[1, 2], {"a": 1} | Yes |
[] | No |
null | No |
This can be customized via EvaluationConfig. See Configuration.
Arithmetic Operators
Mathematical operations with type coercion support.
+ (Add)
Add numbers together, or concatenate strings.
Syntax:
{ "+": [a, b, ...] }
{ "+": value }
Arguments:
a,b, … - Values to add (variadic)- Single value is cast to number
Returns: Sum of all arguments, or concatenated string.
Examples:
// Basic addition
{ "+": [1, 2] }
// Result: 3
// Multiple values
{ "+": [1, 2, 3, 4] }
// Result: 10
// Type coercion
{ "+": ["5", 3] }
// Result: 8 (string "5" converted to number)
// Unary plus (convert to number)
{ "+": "42" }
// Result: 42
{ "+": "-3.14" }
// Result: -3.14
// With variables
{ "+": [{ "var": "price" }, { "var": "tax" }] }
// Data: { "price": 100, "tax": 8.5 }
// Result: 108.5
Try it:
Notes:
- Strings are converted to numbers when possible
- Non-numeric strings may result in NaN or error (configurable)
- Single argument converts value to number
- (Subtract)
Subtract numbers.
Syntax:
{ "-": [a, b] }
{ "-": value }
Arguments:
a- Value to subtract fromb- Value to subtract- Single value negates it
Returns: Difference, or negated value.
Examples:
// Subtraction
{ "-": [10, 3] }
// Result: 7
// Unary minus (negate)
{ "-": 5 }
// Result: -5
{ "-": -3 }
// Result: 3
// With coercion
{ "-": ["10", "3"] }
// Result: 7
// Calculate discount
{ "-": [{ "var": "price" }, { "var": "discount" }] }
// Data: { "price": 100, "discount": 15 }
// Result: 85
Try it:
* (Multiply)
Multiply numbers.
Syntax:
{ "*": [a, b, ...] }
Arguments:
a,b, … - Values to multiply (variadic)
Returns: Product of all arguments.
Examples:
// Basic multiplication
{ "*": [3, 4] }
// Result: 12
// Multiple values
{ "*": [2, 3, 4] }
// Result: 24
// With coercion
{ "*": ["5", 2] }
// Result: 10
// Calculate total
{ "*": [{ "var": "quantity" }, { "var": "price" }] }
// Data: { "quantity": 3, "price": 25 }
// Result: 75
// Apply percentage
{ "*": [{ "var": "amount" }, 0.1] }
// Data: { "amount": 200 }
// Result: 20
Try it:
/ (Divide)
Divide numbers.
Syntax:
{ "/": [a, b] }
Arguments:
a- Dividendb- Divisor
Returns: Quotient.
Examples:
// Basic division
{ "/": [10, 2] }
// Result: 5
// Decimal result
{ "/": [7, 2] }
// Result: 3.5
// Division by zero (configurable behavior)
{ "/": [10, 0] }
// Result: Infinity (default) or error
// With coercion
{ "/": ["100", "4"] }
// Result: 25
// Calculate average
{ "/": [{ "+": [10, 20, 30] }, 3] }
// Result: 20
Try it:
Notes:
- Division by zero behavior is configurable via
EvaluationConfig - Default returns
Infinityor-Infinity
% (Modulo)
Calculate remainder of division.
Syntax:
{ "%": [a, b] }
Arguments:
a- Dividendb- Divisor
Returns: Remainder after division.
Examples:
// Basic modulo
{ "%": [10, 3] }
// Result: 1
{ "%": [10, 5] }
// Result: 0
// Negative numbers
{ "%": [-10, 3] }
// Result: -1
// Check if even
{ "==": [{ "%": [{ "var": "n" }, 2] }, 0] }
// Data: { "n": 4 }
// Result: true
Try it:
max
Find the maximum value.
Syntax:
{ "max": [a, b, ...] }
{ "max": array }
Arguments:
a,b, … - Values to compare, orarray- Single array of values
Returns: The largest value.
Examples:
// Multiple arguments
{ "max": [1, 5, 3] }
// Result: 5
// Single array
{ "max": [[1, 5, 3]] }
// Result: 5
// With variables
{ "max": [{ "var": "scores" }] }
// Data: { "scores": [85, 92, 78] }
// Result: 92
// Empty array
{ "max": [[]] }
// Result: null
Try it:
min
Find the minimum value.
Syntax:
{ "min": [a, b, ...] }
{ "min": array }
Arguments:
a,b, … - Values to compare, orarray- Single array of values
Returns: The smallest value.
Examples:
// Multiple arguments
{ "min": [5, 1, 3] }
// Result: 1
// Single array
{ "min": [[5, 1, 3]] }
// Result: 1
// With variables
{ "min": [{ "var": "prices" }] }
// Data: { "prices": [29.99, 19.99, 39.99] }
// Result: 19.99
// Empty array
{ "min": [[]] }
// Result: null
Try it:
abs
Get the absolute value.
Syntax:
{ "abs": value }
Arguments:
value- Number to get absolute value of
Returns: Absolute (positive) value.
Examples:
{ "abs": -5 }
// Result: 5
{ "abs": 5 }
// Result: 5
{ "abs": -3.14 }
// Result: 3.14
{ "abs": 0 }
// Result: 0
// Distance between two points
{ "abs": { "-": [{ "var": "a" }, { "var": "b" }] } }
// Data: { "a": 3, "b": 10 }
// Result: 7
Try it:
ceil
Round up to the nearest integer.
Syntax:
{ "ceil": value }
Arguments:
value- Number to round up
Returns: Smallest integer greater than or equal to value.
Examples:
{ "ceil": 4.1 }
// Result: 5
{ "ceil": 4.9 }
// Result: 5
{ "ceil": 4.0 }
// Result: 4
{ "ceil": -4.1 }
// Result: -4
// Round up to whole units
{ "ceil": { "/": [{ "var": "items" }, 10] } }
// Data: { "items": 25 }
// Result: 3 (need 3 boxes of 10)
Try it:
floor
Round down to the nearest integer.
Syntax:
{ "floor": value }
Arguments:
value- Number to round down
Returns: Largest integer less than or equal to value.
Examples:
{ "floor": 4.9 }
// Result: 4
{ "floor": 4.1 }
// Result: 4
{ "floor": 4.0 }
// Result: 4
{ "floor": -4.1 }
// Result: -5
// Truncate decimal
{ "floor": { "var": "amount" } }
// Data: { "amount": 99.99 }
// Result: 99
Try it:
Control Flow Operators
Conditional branching and value selection operators.
if
Conditional branching with if/then/else chains.
Syntax:
{ "if": [condition, then_value] }
{ "if": [condition, then_value, else_value] }
{ "if": [cond1, value1, cond2, value2, ..., else_value] }
Arguments:
condition- Condition to evaluatethen_value- Value if condition is truthyelse_value- Value if condition is falsy (optional)- Additional condition/value pairs for else-if chains
Returns: The value corresponding to the first truthy condition, or the else value.
Examples:
// Simple if/then
{ "if": [true, "yes"] }
// Result: "yes"
{ "if": [false, "yes"] }
// Result: null
// If/then/else
{ "if": [true, "yes", "no"] }
// Result: "yes"
{ "if": [false, "yes", "no"] }
// Result: "no"
// If/else-if/else chain
{ "if": [
{ ">=": [{ "var": "score" }, 90] }, "A",
{ ">=": [{ "var": "score" }, 80] }, "B",
{ ">=": [{ "var": "score" }, 70] }, "C",
{ ">=": [{ "var": "score" }, 60] }, "D",
"F"
]}
// Data: { "score": 85 }
// Result: "B"
// Nested if
{ "if": [
{ "var": "premium" },
{ "if": [
{ ">": [{ "var": "amount" }, 100] },
"free_shipping",
"standard_shipping"
]},
"no_shipping"
]}
// Data: { "premium": true, "amount": 150 }
// Result: "free_shipping"
Try it:
Notes:
- Only evaluates the matching branch (lazy evaluation)
- Empty condition list returns
null - Odd number of arguments uses last as else value
?: (Ternary)
Ternary conditional operator (shorthand if/then/else).
Syntax:
{ "?:": [condition, then_value, else_value] }
Arguments:
condition- Condition to evaluatethen_value- Value if condition is truthyelse_value- Value if condition is falsy
Returns: then_value if condition is truthy, else_value otherwise.
Examples:
// Basic ternary
{ "?:": [true, "yes", "no"] }
// Result: "yes"
{ "?:": [false, "yes", "no"] }
// Result: "no"
// With comparison
{ "?:": [
{ ">": [{ "var": "age" }, 18] },
"adult",
"minor"
]}
// Data: { "age": 21 }
// Result: "adult"
// Nested ternary
{ "?:": [
{ "var": "vip" },
0,
{ "?:": [
{ ">": [{ "var": "total" }, 50] },
5,
10
]}
]}
// Data: { "vip": false, "total": 75 }
// Result: 5 (shipping cost)
Try it:
Notes:
- Equivalent to
{ "if": [condition, then_value, else_value] } - More concise for simple conditions
- Only evaluates the matching branch
?? (Null Coalesce)
Return the first non-null value.
Syntax:
{ "??": [a, b] }
{ "??": [a, b, c, ...] }
Arguments:
a,b, … - Values to check (variadic)
Returns: The first non-null value, or null if all are null.
Examples:
// First is not null
{ "??": ["hello", "default"] }
// Result: "hello"
// First is null
{ "??": [null, "default"] }
// Result: "default"
// Multiple values
{ "??": [null, null, "found"] }
// Result: "found"
// All null
{ "??": [null, null] }
// Result: null
// With variables (default value pattern)
{ "??": [{ "var": "nickname" }, { "var": "name" }, "Anonymous"] }
// Data: { "name": "Alice" }
// Result: "Alice"
// Note: 0, "", and false are NOT null
{ "??": [0, "default"] }
// Result: 0
{ "??": ["", "default"] }
// Result: ""
{ "??": [false, "default"] }
// Result: false
Try it:
Notes:
- Only checks for
null, not other falsy values - Use
orif you want to skip all falsy values - Short-circuits: stops at first non-null value
Comparison: if vs ?: vs ?? vs or
| Operator | Use Case | Falsy Handling |
|---|---|---|
if | Complex branching, multiple conditions | Evaluates truthiness |
?: | Simple if/else | Evaluates truthiness |
?? | Default for null only | Only skips null |
or | Default for any falsy | Skips all falsy values |
Examples:
// Value is 0 (falsy but not null)
// Data: { "count": 0 }
{ "if": [{ "var": "count" }, { "var": "count" }, 10] }
// Result: 10 (0 is falsy)
{ "?:": [{ "var": "count" }, { "var": "count" }, 10] }
// Result: 10 (0 is falsy)
{ "??": [{ "var": "count" }, 10] }
// Result: 0 (0 is not null)
{ "or": [{ "var": "count" }, 10] }
// Result: 10 (0 is falsy)
Choose the operator based on whether you want to treat 0, "", and false as valid values.
String Operators
String manipulation and searching operations.
cat
Concatenate strings together.
Syntax:
{ "cat": [a, b, ...] }
Arguments:
a,b, … - Values to concatenate (variadic)
Returns: Concatenated string.
Examples:
// Simple concatenation
{ "cat": ["Hello", " ", "World"] }
// Result: "Hello World"
// With variables
{ "cat": ["Hello, ", { "var": "name" }, "!"] }
// Data: { "name": "Alice" }
// Result: "Hello, Alice!"
// Non-strings are converted
{ "cat": ["Value: ", 42] }
// Result: "Value: 42"
{ "cat": ["Is active: ", true] }
// Result: "Is active: true"
// Building paths
{ "cat": ["/users/", { "var": "userId" }, "/profile"] }
// Data: { "userId": 123 }
// Result: "/users/123/profile"
Try it:
substr
Extract a substring.
Syntax:
{ "substr": [string, start] }
{ "substr": [string, start, length] }
Arguments:
string- Source stringstart- Starting index (0-based, negative counts from end)length- Number of characters (optional, negative counts from end)
Returns: Extracted substring.
Examples:
// From start index
{ "substr": ["Hello World", 0, 5] }
// Result: "Hello"
// From middle
{ "substr": ["Hello World", 6] }
// Result: "World"
// Negative start (from end)
{ "substr": ["Hello World", -5] }
// Result: "World"
// Negative length (exclude from end)
{ "substr": ["Hello World", 0, -6] }
// Result: "Hello"
// Get file extension
{ "substr": ["document.pdf", -3] }
// Result: "pdf"
// With variables
{ "substr": [{ "var": "text" }, 0, 10] }
// Data: { "text": "This is a long string" }
// Result: "This is a "
Try it:
in
Check if a value is contained in a string or array.
Syntax:
{ "in": [needle, haystack] }
Arguments:
needle- Value to search forhaystack- String or array to search in
Returns: true if found, false otherwise.
Examples:
// String contains substring
{ "in": ["World", "Hello World"] }
// Result: true
{ "in": ["xyz", "Hello World"] }
// Result: false
// Array contains element
{ "in": [2, [1, 2, 3]] }
// Result: true
{ "in": [5, [1, 2, 3]] }
// Result: false
// Check membership
{ "in": [{ "var": "role" }, ["admin", "moderator"]] }
// Data: { "role": "admin" }
// Result: true
// Check substring
{ "in": ["@", { "var": "email" }] }
// Data: { "email": "user@example.com" }
// Result: true
Try it:
length
Get the length of a string or array.
Syntax:
{ "length": value }
Arguments:
value- String or array
Returns: Length (number of characters or elements).
Examples:
// String length
{ "length": "Hello" }
// Result: 5
// Array length
{ "length": [1, 2, 3, 4, 5] }
// Result: 5
// Empty values
{ "length": "" }
// Result: 0
{ "length": [] }
// Result: 0
// With variables
{ "length": { "var": "items" } }
// Data: { "items": ["a", "b", "c"] }
// Result: 3
// Check minimum length
{ ">=": [{ "length": { "var": "password" } }, 8] }
// Data: { "password": "secret123" }
// Result: true
Try it:
starts_with
Check if a string starts with a prefix.
Syntax:
{ "starts_with": [string, prefix] }
Arguments:
string- String to checkprefix- Prefix to look for
Returns: true if string starts with prefix, false otherwise.
Examples:
{ "starts_with": ["Hello World", "Hello"] }
// Result: true
{ "starts_with": ["Hello World", "World"] }
// Result: false
// Check URL scheme
{ "starts_with": [{ "var": "url" }, "https://"] }
// Data: { "url": "https://example.com" }
// Result: true
// Case sensitive
{ "starts_with": ["Hello", "hello"] }
// Result: false
Try it:
ends_with
Check if a string ends with a suffix.
Syntax:
{ "ends_with": [string, suffix] }
Arguments:
string- String to checksuffix- Suffix to look for
Returns: true if string ends with suffix, false otherwise.
Examples:
{ "ends_with": ["Hello World", "World"] }
// Result: true
{ "ends_with": ["Hello World", "Hello"] }
// Result: false
// Check file extension
{ "ends_with": [{ "var": "filename" }, ".pdf"] }
// Data: { "filename": "report.pdf" }
// Result: true
// Case sensitive
{ "ends_with": ["test.PDF", ".pdf"] }
// Result: false
Try it:
upper
Convert string to uppercase.
Syntax:
{ "upper": string }
Arguments:
string- String to convert
Returns: Uppercase string.
Examples:
{ "upper": "hello" }
// Result: "HELLO"
{ "upper": "Hello World" }
// Result: "HELLO WORLD"
// With variable
{ "upper": { "var": "name" } }
// Data: { "name": "alice" }
// Result: "ALICE"
Try it:
lower
Convert string to lowercase.
Syntax:
{ "lower": string }
Arguments:
string- String to convert
Returns: Lowercase string.
Examples:
{ "lower": "HELLO" }
// Result: "hello"
{ "lower": "Hello World" }
// Result: "hello world"
// Case-insensitive comparison
{ "==": [
{ "lower": { "var": "input" } },
"yes"
]}
// Data: { "input": "YES" }
// Result: true
Try it:
trim
Remove leading and trailing whitespace.
Syntax:
{ "trim": string }
Arguments:
string- String to trim
Returns: String with whitespace removed from both ends.
Examples:
{ "trim": " hello " }
// Result: "hello"
{ "trim": "\n\ttext\n\t" }
// Result: "text"
// Clean user input
{ "trim": { "var": "userInput" } }
// Data: { "userInput": " search query " }
// Result: "search query"
Try it:
split
Split a string into an array.
Syntax:
{ "split": [string, delimiter] }
Arguments:
string- String to splitdelimiter- Delimiter to split on
Returns: Array of substrings.
Examples:
// Split by space
{ "split": ["Hello World", " "] }
// Result: ["Hello", "World"]
// Split by comma
{ "split": ["a,b,c", ","] }
// Result: ["a", "b", "c"]
// Split by empty string (characters)
{ "split": ["abc", ""] }
// Result: ["a", "b", "c"]
// Parse CSV-like data
{ "split": [{ "var": "tags" }, ","] }
// Data: { "tags": "rust,json,logic" }
// Result: ["rust", "json", "logic"]
// Get first part
{ "var": "0" }
// Applied to: { "split": ["user@example.com", "@"] }
// Result: "user"
Try it:
Array Operators
Operations for working with arrays, including iteration and transformation.
merge
Merge multiple arrays into one.
Syntax:
{ "merge": [array1, array2, ...] }
Arguments:
array1,array2, … - Arrays to merge
Returns: Single flattened array.
Examples:
// Merge two arrays
{ "merge": [[1, 2], [3, 4]] }
// Result: [1, 2, 3, 4]
// Merge multiple
{ "merge": [[1], [2], [3]] }
// Result: [1, 2, 3]
// Non-arrays are wrapped
{ "merge": [[1, 2], 3, [4, 5]] }
// Result: [1, 2, 3, 4, 5]
// With variables
{ "merge": [{ "var": "arr1" }, { "var": "arr2" }] }
// Data: { "arr1": [1, 2], "arr2": [3, 4] }
// Result: [1, 2, 3, 4]
Try it:
filter
Filter array elements based on a condition.
Syntax:
{ "filter": [array, condition] }
Arguments:
array- Array to filtercondition- Condition applied to each element (use{"var": ""}for current element)
Returns: Array of elements where condition is truthy.
Examples:
// Filter numbers greater than 2
{ "filter": [
[1, 2, 3, 4, 5],
{ ">": [{ "var": "" }, 2] }
]}
// Result: [3, 4, 5]
// Filter even numbers
{ "filter": [
[1, 2, 3, 4, 5, 6],
{ "==": [{ "%": [{ "var": "" }, 2] }, 0] }
]}
// Result: [2, 4, 6]
// Filter objects by property
{ "filter": [
{ "var": "users" },
{ "==": [{ "var": "active" }, true] }
]}
// Data: {
// "users": [
// { "name": "Alice", "active": true },
// { "name": "Bob", "active": false },
// { "name": "Carol", "active": true }
// ]
// }
// Result: [{ "name": "Alice", "active": true }, { "name": "Carol", "active": true }]
// Filter with multiple conditions
{ "filter": [
{ "var": "products" },
{ "and": [
{ ">": [{ "var": "price" }, 10] },
{ "var": "inStock" }
]}
]}
Try it:
Notes:
- Inside the condition,
{"var": ""}refers to the current element - The original array is not modified
map
Transform each element of an array.
Syntax:
{ "map": [array, transformation] }
Arguments:
array- Array to transformtransformation- Operation applied to each element
Returns: Array of transformed elements.
Examples:
// Double each number
{ "map": [
[1, 2, 3],
{ "*": [{ "var": "" }, 2] }
]}
// Result: [2, 4, 6]
// Extract property from objects
{ "map": [
{ "var": "users" },
{ "var": "name" }
]}
// Data: {
// "users": [
// { "name": "Alice", "age": 30 },
// { "name": "Bob", "age": 25 }
// ]
// }
// Result: ["Alice", "Bob"]
// Create new objects
{ "map": [
{ "var": "items" },
{ "cat": ["Item: ", { "var": "name" }] }
]}
// Data: { "items": [{ "name": "A" }, { "name": "B" }] }
// Result: ["Item: A", "Item: B"]
// Square numbers
{ "map": [
[1, 2, 3, 4],
{ "*": [{ "var": "" }, { "var": "" }] }
]}
// Result: [1, 4, 9, 16]
Try it:
reduce
Reduce an array to a single value.
Syntax:
{ "reduce": [array, reducer, initial] }
Arguments:
array- Array to reducereducer- Operation combining accumulator and current elementinitial- Initial value for accumulator
Returns: Final accumulated value.
Context Variables:
{"var": "current"}- Current element{"var": "accumulator"}- Current accumulated value
Examples:
// Sum all numbers
{ "reduce": [
[1, 2, 3, 4, 5],
{ "+": [{ "var": "accumulator" }, { "var": "current" }] },
0
]}
// Result: 15
// Product of all numbers
{ "reduce": [
[1, 2, 3, 4],
{ "*": [{ "var": "accumulator" }, { "var": "current" }] },
1
]}
// Result: 24
// Concatenate strings
{ "reduce": [
["a", "b", "c"],
{ "cat": [{ "var": "accumulator" }, { "var": "current" }] },
""
]}
// Result: "abc"
// Find maximum
{ "reduce": [
[3, 1, 4, 1, 5, 9],
{ "if": [
{ ">": [{ "var": "current" }, { "var": "accumulator" }] },
{ "var": "current" },
{ "var": "accumulator" }
]},
0
]}
// Result: 9
// Count elements matching condition
{ "reduce": [
[1, 2, 3, 4, 5, 6],
{ "+": [
{ "var": "accumulator" },
{ "if": [{ ">": [{ "var": "current" }, 3] }, 1, 0] }
]},
0
]}
// Result: 3 (count of numbers > 3)
Try it:
all
Check if all elements satisfy a condition.
Syntax:
{ "all": [array, condition] }
Arguments:
array- Array to checkcondition- Condition applied to each element
Returns: true if all elements satisfy condition, false otherwise.
Examples:
// All positive
{ "all": [
[1, 2, 3],
{ ">": [{ "var": "" }, 0] }
]}
// Result: true
// All greater than 5
{ "all": [
[1, 2, 3],
{ ">": [{ "var": "" }, 5] }
]}
// Result: false
// All users active
{ "all": [
{ "var": "users" },
{ "var": "active" }
]}
// Data: { "users": [{ "active": true }, { "active": true }] }
// Result: true
// Empty array returns true (vacuous truth)
{ "all": [[], { ">": [{ "var": "" }, 0] }] }
// Result: true
Try it:
some
Check if any element satisfies a condition.
Syntax:
{ "some": [array, condition] }
Arguments:
array- Array to checkcondition- Condition applied to each element
Returns: true if at least one element satisfies condition, false otherwise.
Examples:
// Any negative
{ "some": [
[1, -2, 3],
{ "<": [{ "var": "" }, 0] }
]}
// Result: true
// Any greater than 10
{ "some": [
[1, 2, 3],
{ ">": [{ "var": "" }, 10] }
]}
// Result: false
// Any admin user
{ "some": [
{ "var": "users" },
{ "==": [{ "var": "role" }, "admin"] }
]}
// Data: {
// "users": [
// { "role": "user" },
// { "role": "admin" }
// ]
// }
// Result: true
// Empty array returns false
{ "some": [[], { ">": [{ "var": "" }, 0] }] }
// Result: false
Try it:
none
Check if no elements satisfy a condition.
Syntax:
{ "none": [array, condition] }
Arguments:
array- Array to checkcondition- Condition applied to each element
Returns: true if no elements satisfy condition, false otherwise.
Examples:
// None negative
{ "none": [
[1, 2, 3],
{ "<": [{ "var": "" }, 0] }
]}
// Result: true
// None greater than 0
{ "none": [
[1, 2, 3],
{ ">": [{ "var": "" }, 0] }
]}
// Result: false
// No banned users
{ "none": [
{ "var": "users" },
{ "var": "banned" }
]}
// Data: { "users": [{ "banned": false }, { "banned": false }] }
// Result: true
// Empty array returns true
{ "none": [[], { ">": [{ "var": "" }, 0] }] }
// Result: true
Try it:
sort
Sort an array.
Syntax:
{ "sort": array }
{ "sort": [array] }
{ "sort": [array, comparator] }
Arguments:
array- Array to sortcomparator- Optional comparison logic
Returns: Sorted array.
Examples:
// Sort numbers
{ "sort": [[3, 1, 4, 1, 5, 9]] }
// Result: [1, 1, 3, 4, 5, 9]
// Sort strings
{ "sort": [["banana", "apple", "cherry"]] }
// Result: ["apple", "banana", "cherry"]
// Sort with custom comparator
{ "sort": [
{ "var": "items" },
{ "-": [{ "var": "a.price" }, { "var": "b.price" }] }
]}
// Data: {
// "items": [
// { "name": "B", "price": 20 },
// { "name": "A", "price": 10 }
// ]
// }
// Result: [{ "name": "A", "price": 10 }, { "name": "B", "price": 20 }]
Try it:
slice
Extract a portion of an array.
Syntax:
{ "slice": [array, start] }
{ "slice": [array, start, end] }
Arguments:
array- Source arraystart- Starting index (negative counts from end)end- Ending index, exclusive (optional, negative counts from end)
Returns: Array slice.
Examples:
// From index 2 to end
{ "slice": [[1, 2, 3, 4, 5], 2] }
// Result: [3, 4, 5]
// From index 1 to 3
{ "slice": [[1, 2, 3, 4, 5], 1, 3] }
// Result: [2, 3]
// Last 2 elements
{ "slice": [[1, 2, 3, 4, 5], -2] }
// Result: [4, 5]
// First 3 elements
{ "slice": [[1, 2, 3, 4, 5], 0, 3] }
// Result: [1, 2, 3]
// Pagination
{ "slice": [
{ "var": "items" },
{ "*": [{ "var": "page" }, 10] },
{ "+": [{ "*": [{ "var": "page" }, 10] }, 10] }
]}
// Data: { "items": [...], "page": 0 }
// Result: first 10 items
Try it:
DateTime Operators
Operations for working with dates, times, and durations.
now
Get the current UTC datetime.
Syntax:
{ "now": [] }
Arguments: None
Returns: Current UTC datetime as ISO 8601 string.
Examples:
{ "now": [] }
// Result: "2024-01-15T14:30:00Z" (current time)
// Check if date is in the future
{ ">": [{ "var": "expiresAt" }, { "now": [] }] }
// Data: { "expiresAt": "2025-12-31T00:00:00Z" }
// Result: true or false depending on current time
// Check if event is happening now
{ "and": [
{ "<=": [{ "var": "startTime" }, { "now": [] }] },
{ ">=": [{ "var": "endTime" }, { "now": [] }] }
]}
Try it:
Notes:
- Returns ISO 8601 formatted string (e.g., “2024-01-15T14:30:00Z”)
- Always returns UTC time
- Useful for time-based conditions and comparisons
datetime
Parse or validate a datetime value.
Syntax:
{ "datetime": value }
Arguments:
value- ISO 8601 datetime string
Returns: The validated datetime string (preserving timezone information).
Examples:
// Parse ISO string
{ "datetime": "2024-01-01T00:00:00Z" }
// Result: "2024-01-01T00:00:00Z"
// With timezone offset
{ "datetime": "2024-01-01T10:00:00+05:30" }
// Result: "2024-01-01T10:00:00+05:30"
// Compare datetimes
{ ">": [
{ "datetime": "2024-06-15T00:00:00Z" },
{ "datetime": "2024-01-01T00:00:00Z" }
]}
// Result: true
// Add duration to datetime
{ "+": [
{ "datetime": "2024-01-01T00:00:00Z" },
{ "timestamp": "7d" }
]}
// Result: "2024-01-08T00:00:00Z"
Try it:
timestamp
Create or parse a duration value. Durations represent time periods (not points in time).
Syntax:
{ "timestamp": duration_string }
Arguments:
duration_string- Duration in format like “1d:2h:3m:4s” or partial like “1d”, “2h”, “30m”, “45s”
Returns: Normalized duration string in format “Xd:Xh:Xm:Xs”.
Duration Format:
d- Daysh- Hoursm- Minutess- Seconds
Examples:
// Full duration format
{ "timestamp": "1d:2h:3m:4s" }
// Result: "1d:2h:3m:4s"
// Days only
{ "timestamp": "2d" }
// Result: "2d:0h:0m:0s"
// Hours only
{ "timestamp": "5h" }
// Result: "0d:5h:0m:0s"
// Minutes only
{ "timestamp": "30m" }
// Result: "0d:0h:30m:0s"
// Compare durations
{ ">": [{ "timestamp": "2d" }, { "timestamp": "36h" }] }
// Result: true (2 days > 36 hours)
// Duration equality
{ "==": [{ "timestamp": "1d" }, { "timestamp": "24h" }] }
// Result: true
Try it:
Duration Arithmetic
Durations can be used in arithmetic operations:
// Multiply duration
{ "*": [{ "timestamp": "1d" }, 2] }
// Result: "2d:0h:0m:0s"
// Divide duration
{ "/": [{ "timestamp": "2d" }, 2] }
// Result: "1d:0h:0m:0s"
// Add durations
{ "+": [{ "timestamp": "1d" }, { "timestamp": "12h" }] }
// Result: "1d:12h:0m:0s"
// Subtract durations
{ "-": [{ "timestamp": "2d" }, { "timestamp": "12h" }] }
// Result: "1d:12h:0m:0s"
// Add duration to datetime
{ "+": [
{ "datetime": "2024-01-01T00:00:00Z" },
{ "timestamp": "7d" }
]}
// Result: "2024-01-08T00:00:00Z"
// Subtract duration from datetime
{ "-": [
{ "datetime": "2024-01-15T00:00:00Z" },
{ "timestamp": "7d" }
]}
// Result: "2024-01-08T00:00:00Z"
// Difference between two datetimes (returns duration)
{ "-": [
{ "datetime": "2024-01-08T00:00:00Z" },
{ "datetime": "2024-01-01T00:00:00Z" }
]}
// Result: "7d:0h:0m:0s"
parse_date
Parse a date string with a custom format into an ISO datetime.
Syntax:
{ "parse_date": [string, format] }
Arguments:
string- Date string to parseformat- Format string using simplified tokens
Returns: Parsed datetime as ISO 8601 string.
Format Tokens:
| Token | Description | Example |
|---|---|---|
yyyy | 4-digit year | 2024 |
MM | 2-digit month | 01-12 |
dd | 2-digit day | 01-31 |
HH | 2-digit hour (24h) | 00-23 |
mm | 2-digit minute | 00-59 |
ss | 2-digit second | 00-59 |
Examples:
// Parse US date format
{ "parse_date": ["12/25/2024", "MM/dd/yyyy"] }
// Result: "2024-12-25T00:00:00Z"
// Parse European format
{ "parse_date": ["25-12-2024", "dd-MM-yyyy"] }
// Result: "2024-12-25T00:00:00Z"
// Parse date only
{ "parse_date": ["2024-01-15", "yyyy-MM-dd"] }
// Result: "2024-01-15T00:00:00Z"
// With variable
{ "parse_date": [{ "var": "dateStr" }, "yyyy-MM-dd"] }
// Data: { "dateStr": "2024-06-15" }
// Result: "2024-06-15T00:00:00Z"
Try it:
format_date
Format a datetime as a string with a custom format.
Syntax:
{ "format_date": [datetime, format] }
Arguments:
datetime- Datetime value to formatformat- Format string using simplified tokens (same as parse_date)
Returns: Formatted date string.
Special Format:
z- Returns timezone offset (e.g., “+0500”)
Examples:
// Format as date only
{ "format_date": [{ "datetime": "2024-01-15T14:30:00Z" }, "yyyy-MM-dd"] }
// Result: "2024-01-15"
// Format as US date
{ "format_date": [{ "datetime": "2024-12-25T00:00:00Z" }, "MM/dd/yyyy"] }
// Result: "12/25/2024"
// Get timezone offset
{ "format_date": [{ "datetime": "2024-01-01T10:00:00+05:00" }, "z"] }
// Result: "+0500"
// Format current time
{ "format_date": [{ "now": [] }, "yyyy-MM-dd"] }
// Result: "2024-01-15" (current date)
// With variable
{ "format_date": [{ "var": "date" }, "dd/MM/yyyy"] }
// Data: { "date": "2024-12-25T00:00:00Z" }
// Result: "25/12/2024"
Try it:
date_diff
Calculate the difference between two dates in a specified unit.
Syntax:
{ "date_diff": [date1, date2, unit] }
Arguments:
date1- First datetimedate2- Second datetimeunit- Unit of measurement: “days”, “hours”, “minutes”, “seconds”
Returns: Difference as an integer in the specified unit.
Examples:
// Days between dates
{ "date_diff": [
{ "datetime": "2024-12-31T00:00:00Z" },
{ "datetime": "2024-01-01T00:00:00Z" },
"days"
]}
// Result: 365
// Hours difference
{ "date_diff": [
{ "datetime": "2024-01-01T12:00:00Z" },
{ "datetime": "2024-01-01T00:00:00Z" },
"hours"
]}
// Result: 12
// With variables
{ "date_diff": [
{ "var": "end" },
{ "var": "start" },
"days"
]}
// Data: {
// "start": "2024-01-01T00:00:00Z",
// "end": "2024-01-15T00:00:00Z"
// }
// Result: 14
// Check if within 24 hours
{ "<": [
{ "date_diff": [{ "now": [] }, { "var": "timestamp" }, "hours"] },
24
]}
// Data: { "timestamp": "2024-01-15T10:00:00Z" }
// Result: true or false
// Days since creation
{ "date_diff": [
{ "now": [] },
{ "var": "createdAt" },
"days"
]}
Try it:
DateTime Patterns
Check if date is in the past
{ "<": [{ "var": "date" }, { "now": [] }] }
Check if date is in the future
{ ">": [{ "var": "date" }, { "now": [] }] }
Check if within time window
{ "and": [
{ ">=": [{ "now": [] }, { "var": "startTime" }] },
{ "<=": [{ "now": [] }, { "var": "endTime" }] }
]}
Add days to a date
{ "+": [
{ "var": "date" },
{ "timestamp": "7d" }
]}
Calculate days until expiration
{ "date_diff": [
{ "var": "expiresAt" },
{ "now": [] },
"days"
]}
Check if expired
{ "<": [{ "var": "expiresAt" }, { "now": [] }] }
Missing Value Operators
Operators for checking if data fields are missing or undefined.
missing
Check for missing fields in the data.
Syntax:
{ "missing": [key1, key2, ...] }
{ "missing": key }
Arguments:
key1,key2, … - Field names to check
Returns: Array of missing field names.
Examples:
// Check single field
{ "missing": "name" }
// Data: { "age": 25 }
// Result: ["name"]
// Check multiple fields
{ "missing": ["name", "email", "phone"] }
// Data: { "name": "Alice", "phone": "555-1234" }
// Result: ["email"]
// All fields present
{ "missing": ["name", "age"] }
// Data: { "name": "Alice", "age": 25 }
// Result: []
// All fields missing
{ "missing": ["name", "age"] }
// Data: {}
// Result: ["name", "age"]
// Nested fields
{ "missing": ["user.name", "user.email"] }
// Data: { "user": { "name": "Alice" } }
// Result: ["user.email"]
Common Patterns
Require all fields:
{ "!": { "missing": ["name", "email", "password"] } }
// Returns true only if all fields are present
Check if any field is missing:
{ "!!": { "missing": ["name", "email"] } }
// Returns true if ANY field is missing
Conditional validation:
{ "if": [
{ "missing": ["required_field"] },
{ "throw": "Missing required field" },
"ok"
]}
Try it:
missing_some
Check if at least N fields are missing from a set.
Syntax:
{ "missing_some": [minimum, [key1, key2, ...]] }
Arguments:
minimum- Minimum number of fields that should be present[key1, key2, ...]- Array of field names to check
Returns: Array of missing field names if fewer than minimum are present, empty array otherwise.
Examples:
// Need at least 1 of these contact methods
{ "missing_some": [1, ["email", "phone", "address"]] }
// Data: { "email": "a@b.com" }
// Result: [] (1 present, requirement met)
// Data: {}
// Result: ["email", "phone", "address"] (0 present, need at least 1)
// Need at least 2 of these
{ "missing_some": [2, ["name", "email", "phone"]] }
// Data: { "name": "Alice" }
// Result: ["email", "phone"] (only 1 present, need 2)
// Data: { "name": "Alice", "email": "a@b.com" }
// Result: [] (2 present, requirement met)
// Data: { "name": "Alice", "email": "a@b.com", "phone": "555" }
// Result: [] (3 present, exceeds requirement)
Common Patterns
Require at least one contact method:
{ "!": { "missing_some": [1, ["email", "phone", "fax"]] } }
// Returns true if at least one contact method is provided
Flexible field requirements:
{ "if": [
{ "missing_some": [2, ["street", "city", "zip", "country"]] },
"Please provide at least 2 address fields",
"Address accepted"
]}
Require majority of fields:
{ "!": { "missing_some": [3, ["field1", "field2", "field3", "field4", "field5"]] } }
// Returns true if at least 3 of 5 fields are present
Try it:
Comparison: missing vs missing_some
| Scenario | missing | missing_some |
|---|---|---|
| All fields required | { "!": { "missing": [...] } } | N/A |
| At least N required | Complex logic needed | { "!": { "missing_some": [N, [...]] } } |
| Check which are missing | Returns missing list | Returns missing list if < N present |
| No minimum | Appropriate | Use with minimum=1 |
Integration with Validation
Form validation example:
{ "if": [
{ "missing": ["username", "password"] },
{ "throw": { "code": "VALIDATION_ERROR", "missing": { "missing": ["username", "password"] } } },
{ "if": [
{ "missing_some": [1, ["email", "phone"]] },
{ "throw": { "code": "CONTACT_REQUIRED", "message": "Provide email or phone" } },
"valid"
]}
]}
Conditional field requirements:
// If business account, require company name
{ "if": [
{ "==": [{ "var": "accountType" }, "business"] },
{ "!": { "missing": ["companyName", "taxId"] } },
true
]}
Error Handling Operators
Operators for throwing and catching errors, providing exception-like error handling in JSONLogic.
try
Catch errors and provide fallback values.
Syntax:
{ "try": [expression, fallback] }
{ "try": [expression, catch_expression] }
Arguments:
expression- Expression that might throw an errorfallback- Value or expression to use if an error occurs
Returns: Result of expression if successful, or fallback value/expression result if an error occurs.
Context in Catch:
When an error is caught, the catch expression can access error details via var:
{ "var": "message" }- Error message{ "var": "code" }- Error code (if thrown with one){ "var": "" }- Entire error object
Examples:
// Simple fallback value
{ "try": [
{ "/": [10, 0] },
0
]}
// Result: 0 (division by zero caught)
// Expression that succeeds
{ "try": [
{ "+": [1, 2] },
0
]}
// Result: 3 (no error, normal result)
// Catch with error access
{ "try": [
{ "throw": { "code": "NOT_FOUND", "message": "User not found" } },
{ "cat": ["Error: ", { "var": "message" }] }
]}
// Result: "Error: User not found"
// Access error code
{ "try": [
{ "throw": { "code": 404 } },
{ "var": "code" }
]}
// Result: 404
// Nested try for multiple error sources
{ "try": [
{ "try": [
{ "var": "data.nested.value" },
{ "throw": "nested access failed" }
]},
"default"
]}
Common Patterns
Safe division:
{ "try": [
{ "/": [{ "var": "numerator" }, { "var": "denominator" }] },
0
]}
Safe property access:
{ "try": [
{ "var": "user.profile.settings.theme" },
"default-theme"
]}
Error logging pattern:
{ "try": [
{ "risky_operation": [] },
{ "cat": ["Operation failed: ", { "var": "message" }] }
]}
Try it:
throw
Throw an error with optional details.
Syntax:
{ "throw": message }
{ "throw": { "code": code, "message": message, ...} }
Arguments:
message- Error message string, or- Error object with
code,message, and additional properties
Returns: Never returns normally; throws an error that must be caught by try.
Examples:
// Simple string error
{ "throw": "Something went wrong" }
// Throws error with message "Something went wrong"
// Error with code
{ "throw": { "code": "INVALID_INPUT", "message": "Age must be positive" } }
// Throws error with code and message
// Error with additional data
{ "throw": {
"code": "VALIDATION_ERROR",
"message": "Invalid email format",
"field": "email",
"value": { "var": "email" }
}}
// Throws detailed error with context
// Conditional throw
{ "if": [
{ "<": [{ "var": "age" }, 0] },
{ "throw": { "code": "INVALID_AGE", "message": "Age cannot be negative" } },
{ "var": "age" }
]}
// Data: { "age": -5 }
// Throws error
// Data: { "age": 25 }
// Result: 25
Common Patterns
Validation with throw:
{ "if": [
{ "missing": ["name", "email"] },
{ "throw": {
"code": "MISSING_FIELDS",
"message": "Required fields missing",
"fields": { "missing": ["name", "email"] }
}},
"valid"
]}
Business rule enforcement:
{ "if": [
{ ">": [{ "var": "amount" }, { "var": "balance" }] },
{ "throw": {
"code": "INSUFFICIENT_FUNDS",
"message": "Amount exceeds balance",
"requested": { "var": "amount" },
"available": { "var": "balance" }
}},
{ "-": [{ "var": "balance" }, { "var": "amount" }] }
]}
Type validation:
{ "if": [
{ "!==": [{ "type": { "var": "value" } }, "number"] },
{ "throw": { "code": "TYPE_ERROR", "message": "Expected number" } },
{ "*": [{ "var": "value" }, 2] }
]}
Try it:
Error Handling Patterns
Graceful Degradation
{ "try": [
{ "var": "user.preferences.language" },
{ "try": [
{ "var": "defaults.language" },
"en"
]}
]}
// Try user preference, then defaults, then hardcoded "en"
Validation Pipeline
{ "try": [
{ "if": [
{ "!": { "var": "input" } },
{ "throw": { "code": "EMPTY", "message": "Input required" } },
{ "if": [
{ "<": [{ "length": { "var": "input" } }, 3] },
{ "throw": { "code": "TOO_SHORT", "message": "Minimum 3 characters" } },
{ "var": "input" }
]}
]},
{ "cat": ["Validation error: ", { "var": "message" }] }
]}
Error Recovery with Retry Logic
{ "try": [
{ "primary_operation": [] },
{ "try": [
{ "fallback_operation": [] },
"all operations failed"
]}
]}
Collecting All Errors
While JSONLogic doesn’t natively support collecting multiple errors, you can structure validations to report all issues:
{
"errors": { "filter": [
[
{ "if": [{ "missing": ["name"] }, "name is required", null] },
{ "if": [{ "missing": ["email"] }, "email is required", null] },
{ "if": [
{ "and": [
{ "!": { "missing": ["email"] } },
{ "!": { "in": ["@", { "var": "email" }] } }
]},
"invalid email format",
null
]}
],
{ "!==": [{ "var": "" }, null] }
]}
}
This returns an array of error messages for all validation failures.
Installation
The @goplasmatic/datalogic package provides WebAssembly bindings for the datalogic-rs engine, bringing high-performance JSONLogic evaluation to JavaScript and TypeScript.
Package Installation
# npm
npm install @goplasmatic/datalogic
# yarn
yarn add @goplasmatic/datalogic
# pnpm
pnpm add @goplasmatic/datalogic
Build Targets
The package includes three build targets optimized for different environments:
| Target | Use Case | Init Required |
|---|---|---|
web | Browser ES Modules, CDN | Yes |
bundler | Webpack, Vite, Rollup | Yes |
nodejs | Node.js (CommonJS/ESM) | No |
Automatic Target Selection
The package’s exports field automatically selects the appropriate target:
// Browser/Bundler - uses web or bundler target
import init, { evaluate } from '@goplasmatic/datalogic';
// Node.js - uses nodejs target
const { evaluate } = require('@goplasmatic/datalogic');
Explicit Target Import
If you need a specific target:
// Web target (ES modules with init)
import init, { evaluate } from '@goplasmatic/datalogic/web';
// Bundler target
import init, { evaluate } from '@goplasmatic/datalogic/bundler';
// Node.js target
import { evaluate } from '@goplasmatic/datalogic/nodejs';
WASM Initialization
For browser and bundler environments, you must initialize the WASM module before using any functions:
import init, { evaluate } from '@goplasmatic/datalogic';
// Initialize once at application startup
await init();
// Now you can use evaluate, CompiledRule, etc.
const result = evaluate('{"==": [1, 1]}', '{}', false);
Note: Node.js does not require initialization - you can use functions immediately after import.
TypeScript Support
The package includes TypeScript declarations. No additional @types package is needed.
import init, { evaluate, CompiledRule, evaluate_with_trace } from '@goplasmatic/datalogic';
// Full type inference for all exports
const result: string = evaluate('{"==": [1, 1]}', '{}', false);
Bundle Size
The WASM binary is approximately 50KB gzipped, making it suitable for web applications where performance is critical.
CDN Usage
For quick prototyping or simple pages, you can load directly from a CDN:
<script type="module">
import init, { evaluate } from 'https://unpkg.com/@goplasmatic/datalogic@latest/web/datalogic_wasm.js';
async function run() {
await init();
console.log(evaluate('{"==": [1, 1]}', '{}', false)); // "true"
}
run();
</script>
Next Steps
- Quick Start - Basic usage examples
- API Reference - Complete API documentation
- Framework Integration - React, Vue, and bundler setup
Quick Start
This guide covers the essential patterns for using JSONLogic in JavaScript/TypeScript.
Basic Evaluation
The simplest way to evaluate JSONLogic:
import init, { evaluate } from '@goplasmatic/datalogic';
// Initialize WASM (required for browser/bundler)
await init();
// Evaluate a simple expression
const result = evaluate('{"==": [1, 1]}', '{}', false);
console.log(result); // "true"
Working with Data
Pass data as a JSON string for variable resolution:
// Access nested data
const logic = '{"var": "user.age"}';
const data = '{"user": {"age": 25}}';
const result = evaluate(logic, data, false);
console.log(result); // "25"
// Multiple variables
const priceLogic = '{"*": [{"var": "price"}, {"var": "quantity"}]}';
const orderData = '{"price": 10.99, "quantity": 3}';
console.log(evaluate(priceLogic, orderData, false)); // "32.97"
Compiled Rules
For repeated evaluation of the same logic, use CompiledRule for better performance:
import init, { CompiledRule } from '@goplasmatic/datalogic';
await init();
// Compile once
const rule = new CompiledRule('{">=": [{"var": "age"}, 18]}', false);
// Evaluate many times with different data
console.log(rule.evaluate('{"age": 21}')); // "true"
console.log(rule.evaluate('{"age": 16}')); // "false"
console.log(rule.evaluate('{"age": 18}')); // "true"
Parsing Results
Results are returned as JSON strings. Parse them for use in your application:
const result = evaluate('{"+": [1, 2, 3]}', '{}', false);
const value = JSON.parse(result); // 6 (number)
// For complex results
const arrayResult = evaluate('{"map": [[1,2,3], {"+": [{"var": ""}, 10]}]}', '{}', false);
const array = JSON.parse(arrayResult); // [11, 12, 13]
Conditional Logic
Use if for branching:
const gradeLogic = JSON.stringify({
"if": [
{ ">=": [{ "var": "score" }, 90] }, "A",
{ ">=": [{ "var": "score" }, 80] }, "B",
{ ">=": [{ "var": "score" }, 70] }, "C",
{ ">=": [{ "var": "score" }, 60] }, "D",
"F"
]
});
const rule = new CompiledRule(gradeLogic, false);
console.log(JSON.parse(rule.evaluate('{"score": 85}'))); // "B"
console.log(JSON.parse(rule.evaluate('{"score": 42}'))); // "F"
Array Operations
Process arrays with map, filter, and reduce:
// Filter items
const filterLogic = JSON.stringify({
"filter": [
{ "var": "items" },
{ ">": [{ "var": ".price" }, 20] }
]
});
const data = JSON.stringify({
items: [
{ name: "Book", price: 15 },
{ name: "Phone", price: 299 },
{ name: "Pen", price: 5 },
{ name: "Headphones", price: 50 }
]
});
const result = JSON.parse(evaluate(filterLogic, data, false));
// [{ name: "Phone", price: 299 }, { name: "Headphones", price: 50 }]
Templating Mode
Enable preserve_structure for JSON templating:
const template = JSON.stringify({
"user": {
"fullName": { "cat": [{ "var": "firstName" }, " ", { "var": "lastName" }] },
"isAdult": { ">=": [{ "var": "age" }, 18] }
},
"timestamp": { "now": [] }
});
const data = JSON.stringify({
firstName: "Alice",
lastName: "Smith",
age: 25
});
// Third parameter = true enables structure preservation
const result = JSON.parse(evaluate(template, data, true));
// {
// "user": { "fullName": "Alice Smith", "isAdult": true },
// "timestamp": "2024-01-15T10:30:00Z"
// }
Error Handling
Wrap evaluations in try-catch:
try {
const result = evaluate('{"invalid": "json', '{}', false);
} catch (error) {
console.error('Evaluation failed:', error);
}
Debugging
Use evaluate_with_trace for step-by-step debugging:
import init, { evaluate_with_trace } from '@goplasmatic/datalogic';
await init();
const trace = evaluate_with_trace(
'{"and": [{"var": "a"}, {"var": "b"}]}',
'{"a": true, "b": false}',
false
);
const traceData = JSON.parse(trace);
console.log('Result:', traceData.result);
console.log('Steps:', traceData.steps);
Next Steps
- API Reference - Complete function documentation
- Framework Integration - React, Vue, and bundler setup
API Reference
Complete API documentation for the @goplasmatic/datalogic WebAssembly package.
Functions
init()
Initialize the WebAssembly module. Required before using any other functions in browser/bundler environments.
function init(input?: InitInput): Promise<InitOutput>;
Parameters:
input(optional) - Custom WASM source (URL, Response, or BufferSource)
Returns: Promise that resolves when initialization is complete
Example:
import init from '@goplasmatic/datalogic';
// Standard initialization
await init();
// Custom WASM location
await init('/custom/path/datalogic_wasm_bg.wasm');
Note: Node.js does not require initialization.
evaluate()
Evaluate a JSONLogic expression against data.
function evaluate(logic: string, data: string, preserve_structure: boolean): string;
Parameters:
logic- JSON string containing the JSONLogic expressiondata- JSON string containing the data contextpreserve_structure- Enable templating mode (preserves object structure)
Returns: JSON string containing the result
Throws: String error message if evaluation fails
Examples:
// Simple comparison
evaluate('{"==": [1, 1]}', '{}', false); // "true"
// Variable access
evaluate('{"var": "name"}', '{"name": "Alice"}', false); // "\"Alice\""
// Arithmetic
evaluate('{"+": [1, 2, 3]}', '{}', false); // "6"
// Array operations
evaluate('{"map": [[1,2,3], {"+": [{"var": ""}, 1]}]}', '{}', false); // "[2,3,4]"
// Templating mode
evaluate(
'{"result": {"var": "x"}, "computed": {"+": [1, 2]}}',
'{"x": 42}',
true
); // '{"result":42,"computed":3}'
evaluate_with_trace()
Evaluate with detailed execution trace for debugging.
function evaluate_with_trace(logic: string, data: string, preserve_structure: boolean): string;
Parameters: Same as evaluate()
Returns: JSON string containing TracedResult:
interface TracedResult {
result: any; // Evaluation result
expression_tree: { // Tree structure of the expression
id: number;
expression: string;
children?: ExpressionNode[];
};
steps: Step[]; // Execution steps
}
interface Step {
node_id: number;
operator: string;
input_values: any[];
output_value: any;
context: any;
}
Example:
const trace = evaluate_with_trace(
'{"and": [true, {"var": "x"}]}',
'{"x": false}',
false
);
const data = JSON.parse(trace);
console.log(data.result); // false
console.log(data.steps.length); // 3 (and, true literal, var lookup)
Classes
CompiledRule
Pre-compiled rule for efficient repeated evaluation.
Constructor
new CompiledRule(logic: string, preserve_structure: boolean)
Parameters:
logic- JSON string containing the JSONLogic expressionpreserve_structure- Enable templating mode
Throws: If the logic is invalid JSON or contains compilation errors
Example:
const rule = new CompiledRule('{">=": [{"var": "age"}, 18]}', false);
Methods
evaluate(data: string): string
Evaluate the compiled rule against data.
evaluate(data: string): string;
Parameters:
data- JSON string containing the data context
Returns: JSON string containing the result
Example:
const rule = new CompiledRule('{"+": [{"var": "a"}, {"var": "b"}]}', false);
rule.evaluate('{"a": 1, "b": 2}'); // "3"
rule.evaluate('{"a": 10, "b": 20}'); // "30"
evaluate_with_trace(data: string): string
Evaluate with execution trace.
evaluate_with_trace(data: string): string;
Parameters:
data- JSON string containing the data context
Returns: JSON string containing TracedResult
Example:
const rule = new CompiledRule('{"if": [{"var": "x"}, "yes", "no"]}', false);
const trace = JSON.parse(rule.evaluate_with_trace('{"x": true}'));
Type Definitions
Input/Output Types
All functions accept and return JSON strings. Parse results for use:
// Input: Always JSON strings
const logic: string = JSON.stringify({ "==": [1, 1] });
const data: string = JSON.stringify({ x: 42 });
// Output: Always JSON strings
const result: string = evaluate(logic, data, false);
const parsed: boolean = JSON.parse(result); // true
Preserve Structure Mode
When preserve_structure is true:
- Unknown object keys become output fields
- Only recognized operators are evaluated
- Useful for JSON templating
// Without preserve_structure - "result" treated as unknown operator
evaluate('{"result": {"var": "x"}}', '{"x": 1}', false);
// Error or unexpected behavior
// With preserve_structure - "result" becomes output field
evaluate('{"result": {"var": "x"}}', '{"x": 1}', true);
// '{"result":1}'
Error Handling
All functions throw string errors on failure:
try {
evaluate('{"invalid json', '{}', false);
} catch (error) {
// error is a string describing the problem
console.error('Failed:', error);
}
Common error types:
- JSON parse errors (invalid syntax)
- Unknown operator errors (in non-preserve mode)
- Type errors (wrong argument types)
- Variable access errors (missing required data)
Performance Tips
-
Use CompiledRule for repeated evaluation:
// Slow: recompiles each time for (const user of users) { evaluate(logic, JSON.stringify(user), false); } // Fast: compile once const rule = new CompiledRule(logic, false); for (const user of users) { rule.evaluate(JSON.stringify(user)); } -
Initialize once at startup:
// Application entry point await init(); // Now use evaluate/CompiledRule anywhere -
Reuse CompiledRule instances:
// Store compiled rules const rules = { isAdult: new CompiledRule('{">=": [{"var": "age"}, 18]}', false), isPremium: new CompiledRule('{"==": [{"var": "tier"}, "premium"]}', false), };
Framework Integration
This guide covers integration with popular JavaScript frameworks and build tools.
React
Basic Setup
import { useEffect, useState } from 'react';
import init, { evaluate, CompiledRule } from '@goplasmatic/datalogic';
function App() {
const [ready, setReady] = useState(false);
useEffect(() => {
init().then(() => setReady(true));
}, []);
if (!ready) return <div>Loading...</div>;
return <RuleEvaluator />;
}
function RuleEvaluator() {
const result = evaluate('{"==": [1, 1]}', '{}', false);
return <div>Result: {result}</div>;
}
Custom Hook
Create a reusable hook for JSONLogic evaluation:
import { useEffect, useState, useMemo } from 'react';
import init, { CompiledRule } from '@goplasmatic/datalogic';
// Initialize once at module level
let initPromise: Promise<void> | null = null;
function ensureInit() {
if (!initPromise) {
initPromise = init();
}
return initPromise;
}
export function useJsonLogic(logic: object, data: unknown) {
const [ready, setReady] = useState(false);
const [result, setResult] = useState<unknown>(null);
const [error, setError] = useState<string | null>(null);
const rule = useMemo(() => {
if (!ready) return null;
try {
return new CompiledRule(JSON.stringify(logic), false);
} catch (e) {
setError(String(e));
return null;
}
}, [logic, ready]);
useEffect(() => {
ensureInit().then(() => setReady(true));
}, []);
useEffect(() => {
if (!rule) return;
try {
const res = rule.evaluate(JSON.stringify(data));
setResult(JSON.parse(res));
setError(null);
} catch (e) {
setError(String(e));
}
}, [rule, data]);
return { result, error, ready };
}
Usage:
function FeatureFlag({ feature, user }) {
const rule = { "and": [
{ "in": [feature, { "var": "enabledFeatures" }] },
{ ">=": [{ "var": "accountAge" }, 30] }
]};
const { result, error, ready } = useJsonLogic(rule, user);
if (!ready) return null;
if (error) return <div>Error: {error}</div>;
return result ? <NewFeature /> : <LegacyFeature />;
}
With React Query
import { useQuery } from '@tanstack/react-query';
import init, { CompiledRule } from '@goplasmatic/datalogic';
export function useCompiledRule(logic: object) {
return useQuery({
queryKey: ['compiled-rule', JSON.stringify(logic)],
queryFn: async () => {
await init();
return new CompiledRule(JSON.stringify(logic), false);
},
staleTime: Infinity,
});
}
Vue
Composition API
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import init, { CompiledRule } from '@goplasmatic/datalogic';
const ready = ref(false);
const data = ref({ age: 25 });
onMounted(async () => {
await init();
ready.value = true;
});
const rule = computed(() => {
if (!ready.value) return null;
return new CompiledRule('{">=": [{"var": "age"}, 18]}', false);
});
const isAdult = computed(() => {
if (!rule.value) return null;
return JSON.parse(rule.value.evaluate(JSON.stringify(data.value)));
});
</script>
<template>
<div v-if="ready">
Is Adult: {{ isAdult }}
</div>
<div v-else>Loading...</div>
</template>
Composable
// useJsonLogic.ts
import { ref, onMounted, watchEffect, Ref } from 'vue';
import init, { CompiledRule } from '@goplasmatic/datalogic';
let initialized = false;
let initPromise: Promise<void> | null = null;
export function useJsonLogic(logic: Ref<object>, data: Ref<unknown>) {
const result = ref<unknown>(null);
const error = ref<string | null>(null);
const ready = ref(false);
onMounted(async () => {
if (!initialized) {
if (!initPromise) initPromise = init();
await initPromise;
initialized = true;
}
ready.value = true;
});
watchEffect(() => {
if (!ready.value) return;
try {
const rule = new CompiledRule(JSON.stringify(logic.value), false);
result.value = JSON.parse(rule.evaluate(JSON.stringify(data.value)));
error.value = null;
} catch (e) {
error.value = String(e);
}
});
return { result, error, ready };
}
Node.js
Express Middleware
const express = require('express');
const { evaluate, CompiledRule } = require('@goplasmatic/datalogic');
const app = express();
app.use(express.json());
// Compile rules at startup
const rules = {
canAccess: new CompiledRule(JSON.stringify({
"and": [
{ "==": [{ "var": "role" }, "admin"] },
{ "var": "active" }
]
}), false)
};
// Middleware
function authorize(ruleName) {
return (req, res, next) => {
const rule = rules[ruleName];
if (!rule) return res.status(500).json({ error: 'Unknown rule' });
const result = JSON.parse(rule.evaluate(JSON.stringify(req.user)));
if (result) {
next();
} else {
res.status(403).json({ error: 'Forbidden' });
}
};
}
app.get('/admin', authorize('canAccess'), (req, res) => {
res.json({ message: 'Welcome, admin!' });
});
Rule Evaluation API
const { evaluate } = require('@goplasmatic/datalogic');
app.post('/api/evaluate', (req, res) => {
const { logic, data, preserveStructure = false } = req.body;
try {
const result = evaluate(
JSON.stringify(logic),
JSON.stringify(data),
preserveStructure
);
res.json({ result: JSON.parse(result) });
} catch (error) {
res.status(400).json({ error: String(error) });
}
});
Bundler Configuration
Vite
WASM works out of the box with Vite:
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
// No special configuration needed
});
Webpack 5
Enable async WASM:
// webpack.config.js
module.exports = {
experiments: {
asyncWebAssembly: true,
},
};
Next.js
// next.config.js
module.exports = {
webpack: (config) => {
config.experiments = {
...config.experiments,
asyncWebAssembly: true,
};
return config;
},
};
For App Router, create a client component:
'use client';
import { useEffect, useState } from 'react';
import init, { evaluate } from '@goplasmatic/datalogic';
export function JsonLogicEvaluator({ logic, data }) {
const [result, setResult] = useState(null);
useEffect(() => {
init().then(() => {
const res = evaluate(JSON.stringify(logic), JSON.stringify(data), false);
setResult(JSON.parse(res));
});
}, [logic, data]);
return <div>{JSON.stringify(result)}</div>;
}
Browser (No Build Tools)
For simple pages without bundlers:
<!DOCTYPE html>
<html>
<head>
<title>JSONLogic Demo</title>
</head>
<body>
<div id="result"></div>
<script type="module">
import init, { evaluate } from 'https://unpkg.com/@goplasmatic/datalogic@latest/web/datalogic_wasm.js';
async function run() {
await init();
const logic = JSON.stringify({ ">=": [{ "var": "age" }, 18] });
const data = JSON.stringify({ age: 21 });
const result = JSON.parse(evaluate(logic, data, false));
document.getElementById('result').textContent =
result ? 'Adult' : 'Minor';
}
run();
</script>
</body>
</html>
Worker Threads
Web Worker
// worker.js
import init, { CompiledRule } from '@goplasmatic/datalogic';
let rule = null;
self.onmessage = async (e) => {
if (e.data.type === 'init') {
await init();
rule = new CompiledRule(e.data.logic, false);
self.postMessage({ type: 'ready' });
} else if (e.data.type === 'evaluate') {
const result = rule.evaluate(JSON.stringify(e.data.data));
self.postMessage({ type: 'result', result: JSON.parse(result) });
}
};
Node.js Worker Thread
const { Worker, isMainThread, parentPort } = require('worker_threads');
const { CompiledRule } = require('@goplasmatic/datalogic');
if (isMainThread) {
const worker = new Worker(__filename);
worker.postMessage({ logic: '{"==": [1, 1]}', data: {} });
worker.on('message', (result) => console.log(result));
} else {
parentPort.on('message', ({ logic, data }) => {
const rule = new CompiledRule(JSON.stringify(logic), false);
const result = JSON.parse(rule.evaluate(JSON.stringify(data)));
parentPort.postMessage(result);
});
}
Installation
The @goplasmatic/datalogic-ui package provides a React component for visualizing and debugging JSONLogic expressions as interactive flow diagrams.
Package Installation
# npm
npm install @goplasmatic/datalogic-ui @xyflow/react
# yarn
yarn add @goplasmatic/datalogic-ui @xyflow/react
# pnpm
pnpm add @goplasmatic/datalogic-ui @xyflow/react
Peer Dependencies
The package requires:
| Package | Version | Purpose |
|---|---|---|
react | 18+ or 19+ | React framework |
react-dom | 18+ or 19+ | React DOM renderer |
@xyflow/react | 12+ | Flow diagram rendering |
Note: The
@goplasmatic/datalogicWASM package is bundled internally for evaluation.
CSS Setup
Import the required styles in your application entry point or component:
// React Flow base styles (required)
import '@xyflow/react/dist/style.css';
// DataLogicEditor styles (required)
import '@goplasmatic/datalogic-ui/styles.css';
Style Import Order
Import order matters. Always import React Flow styles before DataLogicEditor styles:
// Correct order
import '@xyflow/react/dist/style.css';
import '@goplasmatic/datalogic-ui/styles.css';
// Then import components
import { DataLogicEditor } from '@goplasmatic/datalogic-ui';
Minimal Example
import '@xyflow/react/dist/style.css';
import '@goplasmatic/datalogic-ui/styles.css';
import { DataLogicEditor } from '@goplasmatic/datalogic-ui';
function App() {
return (
<div style={{ width: '100%', height: '500px' }}>
<DataLogicEditor
value={{ "==": [{ "var": "x" }, 1] }}
/>
</div>
);
}
Container Requirements
The editor requires a container with defined dimensions:
// Option 1: Explicit dimensions
<div style={{ width: '100%', height: '500px' }}>
<DataLogicEditor value={expression} />
</div>
// Option 2: CSS class
<div className="editor-container">
<DataLogicEditor value={expression} />
</div>
// CSS
.editor-container {
width: 100%;
height: 100vh;
}
TypeScript Setup
Types are included in the package. Import types as needed:
import type {
DataLogicEditorProps,
DataLogicEditorMode,
JsonLogicValue,
} from '@goplasmatic/datalogic-ui';
Bundler Notes
Vite
Works out of the box. No additional configuration needed.
Webpack
Ensure CSS loaders are configured:
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
};
Next.js
For App Router, use client components:
'use client';
import '@xyflow/react/dist/style.css';
import '@goplasmatic/datalogic-ui/styles.css';
import { DataLogicEditor } from '@goplasmatic/datalogic-ui';
export function LogicVisualizer({ expression }) {
return <DataLogicEditor value={expression} />;
}
Next Steps
- Quick Start - Basic usage examples
- Modes - Visualize, debug, and edit modes
- Props & API - Complete props reference
Quick Start
This guide covers essential patterns for using the DataLogicEditor component.
Basic Visualization
Render a JSONLogic expression as a flow diagram:
import '@xyflow/react/dist/style.css';
import '@goplasmatic/datalogic-ui/styles.css';
import { DataLogicEditor } from '@goplasmatic/datalogic-ui';
function App() {
const expression = {
"and": [
{ ">": [{ "var": "age" }, 18] },
{ "==": [{ "var": "status" }, "active"] }
]
};
return (
<div style={{ width: '100%', height: '500px' }}>
<DataLogicEditor value={expression} />
</div>
);
}
Debug Mode
Add evaluation results by providing data context:
function DebugExample() {
const expression = {
"if": [
{ ">=": [{ "var": "score" }, 90] }, "A",
{ ">=": [{ "var": "score" }, 80] }, "B",
"C"
]
};
const userData = {
score: 85
};
return (
<div style={{ width: '100%', height: '500px' }}>
<DataLogicEditor
value={expression}
data={userData}
mode="debug"
/>
</div>
);
}
In debug mode, each node displays its evaluated result, making it easy to trace how the final value was computed.
Dynamic Data
Update evaluation results by changing the data:
import { useState } from 'react';
function DynamicDebugger() {
const [score, setScore] = useState(75);
const expression = {
"if": [
{ ">=": [{ "var": "score" }, 90] }, "A",
{ ">=": [{ "var": "score" }, 80] }, "B",
{ ">=": [{ "var": "score" }, 70] }, "C",
"F"
]
};
return (
<div>
<div>
<label>
Score:
<input
type="range"
min="0"
max="100"
value={score}
onChange={(e) => setScore(Number(e.target.value))}
/>
{score}
</label>
</div>
<div style={{ width: '100%', height: '400px' }}>
<DataLogicEditor
value={expression}
data={{ score }}
mode="debug"
/>
</div>
</div>
);
}
Complex Expressions
The editor handles complex nested expressions:
function ComplexExample() {
const expression = {
"and": [
{ "or": [
{ "==": [{ "var": "user.role" }, "admin"] },
{ "==": [{ "var": "user.role" }, "moderator"] }
]},
{ ">=": [{ "var": "user.accountAge" }, 30] },
{ "!": [{ "var": "user.banned" }] }
]
};
const data = {
user: {
role: "moderator",
accountAge: 45,
banned: false
}
};
return (
<div style={{ width: '100%', height: '600px' }}>
<DataLogicEditor
value={expression}
data={data}
mode="debug"
/>
</div>
);
}
Array Operations
Visualize array operations like map, filter, and reduce:
function ArrayExample() {
const expression = {
"filter": [
{ "var": "items" },
{ ">": [{ "var": ".price" }, 20] }
]
};
const data = {
items: [
{ name: "Book", price: 15 },
{ name: "Phone", price: 299 },
{ name: "Pen", price: 5 }
]
};
return (
<div style={{ width: '100%', height: '400px' }}>
<DataLogicEditor
value={expression}
data={data}
mode="debug"
/>
</div>
);
}
Theme Support
The editor supports light and dark themes:
// Explicit theme
<DataLogicEditor
value={expression}
theme="dark"
/>
// System preference (default)
<DataLogicEditor value={expression} />
// Or set data-theme on a parent element
<div data-theme="dark">
<DataLogicEditor value={expression} />
</div>
Handling Null/Empty Expressions
The editor gracefully handles null or undefined expressions:
function ConditionalEditor({ expression }) {
return (
<div style={{ width: '100%', height: '400px' }}>
<DataLogicEditor
value={expression} // Can be null
/>
</div>
);
}
Styling Container
Add custom styling to the container:
<DataLogicEditor
value={expression}
className="my-custom-editor"
/>
// CSS
.my-custom-editor {
border: 1px solid #ccc;
border-radius: 8px;
}
Next Steps
- Modes - Detailed mode documentation
- Props & API - Complete props reference
- Customization - Theming and styling
Editor Modes
The DataLogicEditor supports three modes, each providing different levels of functionality.
Mode Overview
| Mode | API Value | Description | Requires Data |
|---|---|---|---|
| ReadOnly | 'visualize' | Static diagram visualization | No |
| Debugger | 'debug' | Diagram with evaluation results | Yes |
| Editor | 'edit' | Visual builder (coming soon) | Optional |
Visualize Mode (Default)
The default mode renders a static flow diagram of the JSONLogic expression.
<DataLogicEditor
value={expression}
mode="visualize" // Optional, this is the default
/>
Use cases:
- Documentation and explanation
- Code review and understanding
- Static representation in reports
Features:
- Interactive pan and zoom
- Node highlighting on hover
- Tree-based automatic layout
- Color-coded operator categories
Debug Mode
Debug mode adds evaluation results to each node, showing how the expression evaluates against provided data.
<DataLogicEditor
value={expression}
data={contextData}
mode="debug"
/>
Use cases:
- Understanding evaluation flow
- Debugging unexpected results
- Testing expressions with different inputs
- Learning JSONLogic
Features:
- All visualization features, plus:
- Evaluation results displayed on each node
- Step-by-step execution visibility
- Context values shown for variable nodes
- Highlighted execution path
Debug Mode Requirements
Debug mode requires the data prop. Without it, the component falls back to visualize mode:
// This will work in debug mode
<DataLogicEditor
value={expression}
data={{ x: 1 }}
mode="debug"
/>
// This falls back to visualize mode (no data)
<DataLogicEditor
value={expression}
mode="debug"
/>
Tracing Execution
In debug mode, the component uses evaluate_with_trace internally to capture:
- The result of each sub-expression
- The order of evaluation
- Context values at each step
- Final computed result
Edit Mode (Coming Soon)
Edit mode will provide a full visual builder for creating and modifying JSONLogic expressions.
// Planned API
<DataLogicEditor
value={expression}
onChange={setExpression}
data={contextData} // Optional, for live preview
mode="edit"
/>
Planned features:
- Drag-and-drop node creation
- Visual connection editing
- Operator palette
- Live evaluation preview
- Undo/redo support
- Expression validation
Note: Using
mode="edit"currently renders the component in read-only mode. Ifdatais provided, it shows debug evaluation. A console warning indicates this limitation.
Mode Comparison
Visual Differences
| Aspect | Visualize | Debug | Edit (Planned) |
|---|---|---|---|
| Node display | Structure only | Structure + values | Editable nodes |
| Interactivity | Pan/zoom | Pan/zoom + inspection | Full editing |
| Data required | No | Yes | Optional |
| Output | Static | Static + trace | Two-way bound |
Performance Considerations
- Visualize mode is fastest - no evaluation overhead
- Debug mode runs evaluation on every data change
- Edit mode will include validation and preview costs
For large expressions or frequent data updates, consider debouncing:
import { useMemo } from 'react';
import { useDebouncedValue } from './hooks';
function DebugWithDebounce({ expression, data }) {
const debouncedData = useDebouncedValue(data, 200);
return (
<DataLogicEditor
value={expression}
data={debouncedData}
mode="debug"
/>
);
}
Switching Modes
You can dynamically switch between modes:
function ModeToggle() {
const [mode, setMode] = useState<'visualize' | 'debug'>('visualize');
return (
<div>
<button onClick={() => setMode('visualize')}>Visualize</button>
<button onClick={() => setMode('debug')}>Debug</button>
<DataLogicEditor
value={expression}
data={mode === 'debug' ? data : undefined}
mode={mode}
/>
</div>
);
}
Next Steps
- Props & API - Complete props reference
- Customization - Theming and styling
Props & API Reference
Complete reference for the DataLogicEditor component and related exports.
DataLogicEditor Props
Required Props
value
The JSONLogic expression to render.
value: JsonLogicValue | null
Accepts any valid JSONLogic expression or null for an empty state.
// Simple expression
<DataLogicEditor value={{ "==": [1, 1] }} />
// Complex expression
<DataLogicEditor value={{
"and": [
{ ">=": [{ "var": "age" }, 18] },
{ "var": "active" }
]
}} />
// Null for empty state
<DataLogicEditor value={null} />
Optional Props
mode
The editor mode.
mode?: 'visualize' | 'debug' | 'edit'
Default: 'visualize'
<DataLogicEditor value={expr} mode="debug" />
data
Data context for evaluation (required for debug mode).
data?: unknown
<DataLogicEditor
value={{ "var": "user.name" }}
data={{ user: { name: "Alice" } }}
mode="debug"
/>
onChange
Callback when expression changes (for future edit mode).
onChange?: (expression: JsonLogicValue | null) => void
// Future usage
<DataLogicEditor
value={expression}
onChange={setExpression}
mode="edit"
/>
theme
Theme override.
theme?: 'light' | 'dark'
Default: System preference
<DataLogicEditor value={expr} theme="dark" />
className
Additional CSS class for the container.
className?: string
<DataLogicEditor value={expr} className="my-editor" />
Type Definitions
JsonLogicValue
The type for JSONLogic expressions:
type JsonLogicValue =
| string
| number
| boolean
| null
| JsonLogicValue[]
| { [operator: string]: JsonLogicValue };
DataLogicEditorMode
type DataLogicEditorMode = 'visualize' | 'debug' | 'edit';
DataLogicEditorProps
interface DataLogicEditorProps {
value: JsonLogicValue | null;
onChange?: (expression: JsonLogicValue | null) => void;
data?: unknown;
mode?: DataLogicEditorMode;
theme?: 'light' | 'dark';
className?: string;
}
LogicNode
Internal node type (for advanced customization):
interface LogicNode {
id: string;
type: string;
position: { x: number; y: number };
data: {
label: string;
category: OperatorCategory;
value?: unknown;
result?: unknown;
};
}
LogicEdge
Internal edge type:
interface LogicEdge {
id: string;
source: string;
target: string;
sourceHandle?: string;
targetHandle?: string;
}
OperatorCategory
type OperatorCategory =
| 'logical'
| 'comparison'
| 'arithmetic'
| 'string'
| 'array'
| 'control'
| 'variable'
| 'literal'
| 'datetime'
| 'misc';
Exports
Component
import { DataLogicEditor } from '@goplasmatic/datalogic-ui';
Types
import type {
DataLogicEditorProps,
DataLogicEditorMode,
JsonLogicValue,
LogicNode,
LogicEdge,
OperatorCategory,
} from '@goplasmatic/datalogic-ui';
Constants
import { OPERATORS, CATEGORY_COLORS } from '@goplasmatic/datalogic-ui';
OPERATORS: Map of operator names to their metadata (category, label, etc.)
CATEGORY_COLORS: Color definitions for each operator category
Utilities
import { jsonLogicToNodes, applyTreeLayout } from '@goplasmatic/datalogic-ui';
jsonLogicToNodes: Convert JSONLogic expression to React Flow nodes/edges
const { nodes, edges } = jsonLogicToNodes(expression, traceData?);
applyTreeLayout: Apply dagre tree layout to nodes
const layoutedNodes = applyTreeLayout(nodes, edges, direction?);
Utility Functions
jsonLogicToNodes
Convert a JSONLogic expression to React Flow nodes and edges.
function jsonLogicToNodes(
expression: JsonLogicValue,
trace?: TraceData
): { nodes: LogicNode[]; edges: LogicEdge[] }
Parameters:
expression- JSONLogic expression to converttrace- Optional trace data for debug mode
Returns: Object with nodes and edges arrays
Example:
import { jsonLogicToNodes } from '@goplasmatic/datalogic-ui';
const expr = { "==": [{ "var": "x" }, 1] };
const { nodes, edges } = jsonLogicToNodes(expr);
console.log(nodes);
// [
// { id: '0', type: 'operator', data: { label: '==', category: 'comparison' }, ... },
// { id: '1', type: 'variable', data: { label: 'x', category: 'variable' }, ... },
// { id: '2', type: 'literal', data: { label: '1', category: 'literal' }, ... }
// ]
applyTreeLayout
Apply dagre-based tree layout to nodes.
function applyTreeLayout(
nodes: LogicNode[],
edges: LogicEdge[],
direction?: 'TB' | 'LR'
): LogicNode[]
Parameters:
nodes- Array of nodesedges- Array of edgesdirection- Layout direction (default:'TB'for top-to-bottom)
Returns: Nodes with updated positions
Advanced Usage
Custom Node Rendering
For advanced customization, you can use the utilities to render with your own React Flow setup:
import { ReactFlow } from '@xyflow/react';
import { jsonLogicToNodes, applyTreeLayout } from '@goplasmatic/datalogic-ui';
function CustomEditor({ expression }) {
const { nodes: rawNodes, edges } = jsonLogicToNodes(expression);
const nodes = applyTreeLayout(rawNodes, edges);
return (
<ReactFlow
nodes={nodes}
edges={edges}
nodeTypes={customNodeTypes}
// Custom configuration...
/>
);
}
Accessing Category Colors
import { CATEGORY_COLORS } from '@goplasmatic/datalogic-ui';
// Use in custom styling
const logicalColor = CATEGORY_COLORS.logical; // e.g., '#4CAF50'
Next Steps
- Customization - Theming and styling options
Customization
This guide covers theming, styling, and advanced customization of the DataLogicEditor.
Theming
System Theme (Default)
By default, the editor detects system theme preference:
<DataLogicEditor value={expression} />
Explicit Theme
Override with the theme prop:
// Always dark
<DataLogicEditor value={expression} theme="dark" />
// Always light
<DataLogicEditor value={expression} theme="light" />
Parent-Based Theme
The component respects data-theme on parent elements:
<div data-theme="dark">
<DataLogicEditor value={expression} />
</div>
Dynamic Theme Switching
function ThemedEditor() {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
return (
<div>
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
<DataLogicEditor value={expression} theme={theme} />
</div>
);
}
CSS Customization
Container Styling
Use the className prop for container styling:
<DataLogicEditor value={expression} className="custom-editor" />
.custom-editor {
border: 2px solid #3b82f6;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
CSS Variables
Override CSS variables for global styling:
:root {
/* Node colors by category */
--datalogic-logical-bg: #4caf50;
--datalogic-comparison-bg: #2196f3;
--datalogic-arithmetic-bg: #ff9800;
--datalogic-string-bg: #9c27b0;
--datalogic-array-bg: #00bcd4;
--datalogic-variable-bg: #607d8b;
--datalogic-literal-bg: #795548;
/* General theming */
--datalogic-bg: #ffffff;
--datalogic-text: #1a1a1a;
--datalogic-border: #e5e7eb;
--datalogic-node-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
[data-theme="dark"] {
--datalogic-bg: #1a1a1a;
--datalogic-text: #ffffff;
--datalogic-border: #374151;
--datalogic-node-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
Node Styling
Target specific node types:
/* All nodes */
.react-flow__node {
font-family: 'Inter', sans-serif;
}
/* Operator nodes */
.react-flow__node-operator {
border-width: 2px;
}
/* Variable nodes */
.react-flow__node-variable {
font-style: italic;
}
/* Literal nodes */
.react-flow__node-literal {
font-weight: bold;
}
Edge Styling
Customize connection lines:
.react-flow__edge-path {
stroke: #6b7280;
stroke-width: 2px;
}
.react-flow__edge.selected .react-flow__edge-path {
stroke: #3b82f6;
}
Layout Customization
Container Dimensions
The editor requires explicit dimensions:
// Fixed height
<div style={{ height: '500px' }}>
<DataLogicEditor value={expression} />
</div>
// Viewport height
<div style={{ height: '100vh' }}>
<DataLogicEditor value={expression} />
</div>
// Flexbox
<div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
<header>...</header>
<div style={{ flex: 1 }}>
<DataLogicEditor value={expression} />
</div>
</div>
Using Utilities
Custom Flow Rendering
For complete control, use the utility functions with your own React Flow instance:
import { ReactFlow, Background, Controls } from '@xyflow/react';
import { jsonLogicToNodes, applyTreeLayout, CATEGORY_COLORS } from '@goplasmatic/datalogic-ui';
function CustomEditor({ expression }) {
const { nodes: rawNodes, edges } = jsonLogicToNodes(expression);
const nodes = applyTreeLayout(rawNodes, edges);
return (
<ReactFlow
nodes={nodes}
edges={edges}
fitView
nodesDraggable={false}
nodesConnectable={false}
>
<Background />
<Controls />
</ReactFlow>
);
}
Custom Node Types
Create custom node components:
import { Handle, Position } from '@xyflow/react';
import { CATEGORY_COLORS } from '@goplasmatic/datalogic-ui';
function CustomOperatorNode({ data }) {
const color = CATEGORY_COLORS[data.category];
return (
<div
style={{
background: color,
padding: '12px 20px',
borderRadius: '8px',
color: 'white',
}}
>
<Handle type="target" position={Position.Top} />
<div>{data.label}</div>
{data.result !== undefined && (
<div style={{ fontSize: '0.75em', opacity: 0.8 }}>
= {JSON.stringify(data.result)}
</div>
)}
<Handle type="source" position={Position.Bottom} />
</div>
);
}
const customNodeTypes = {
operator: CustomOperatorNode,
// ... other custom types
};
Category Colors
Access and customize category colors:
import { CATEGORY_COLORS } from '@goplasmatic/datalogic-ui';
// Default colors
console.log(CATEGORY_COLORS);
// {
// logical: '#4CAF50',
// comparison: '#2196F3',
// arithmetic: '#FF9800',
// string: '#9C27B0',
// array: '#00BCD4',
// control: '#F44336',
// variable: '#607D8B',
// literal: '#795548',
// datetime: '#3F51B5',
// misc: '#9E9E9E'
// }
// Use in custom components
function Legend() {
return (
<div>
{Object.entries(CATEGORY_COLORS).map(([category, color]) => (
<div key={category} style={{ display: 'flex', alignItems: 'center' }}>
<span style={{ background: color, width: 16, height: 16 }} />
<span>{category}</span>
</div>
))}
</div>
);
}
Responsive Design
Make the editor responsive:
function ResponsiveEditor({ expression }) {
return (
<div className="editor-wrapper">
<DataLogicEditor value={expression} />
</div>
);
}
.editor-wrapper {
width: 100%;
height: 300px;
}
@media (min-width: 768px) {
.editor-wrapper {
height: 500px;
}
}
@media (min-width: 1024px) {
.editor-wrapper {
height: 700px;
}
}
Performance Tips
Memoization
Memoize expression objects to prevent unnecessary re-renders:
import { useMemo } from 'react';
function OptimizedEditor({ config }) {
const expression = useMemo(() => ({
"and": [
{ ">=": [{ "var": "age" }, config.minAge] },
{ "var": "active" }
]
}), [config.minAge]);
return <DataLogicEditor value={expression} />;
}
Debounced Data Updates
For frequently changing data in debug mode:
import { useDeferredValue } from 'react';
function DebugWithDeferred({ expression, data }) {
const deferredData = useDeferredValue(data);
return (
<DataLogicEditor
value={expression}
data={deferredData}
mode="debug"
/>
);
}
Custom Operators
Extend datalogic-rs with your own operators to implement domain-specific logic.
Basic Custom Operator
Custom operators implement the Operator trait:
#![allow(unused)]
fn main() {
use datalogic_rs::{DataLogic, Operator, ContextStack, Evaluator, Result, Error};
use serde_json::{json, Value};
struct DoubleOperator;
impl Operator for DoubleOperator {
fn evaluate(
&self,
args: &[Value],
context: &mut ContextStack,
evaluator: &dyn Evaluator,
) -> Result<Value> {
// Arguments are unevaluated - must call evaluate() first!
let value = evaluator.evaluate(
args.first().unwrap_or(&Value::Null),
context
)?;
match value.as_f64() {
Some(n) => Ok(json!(n * 2.0)),
None => Err(Error::InvalidArguments("Expected number".to_string()))
}
}
}
}
Registering Custom Operators
Add custom operators to the engine before compiling rules:
#![allow(unused)]
fn main() {
let mut engine = DataLogic::new();
engine.add_operator("double".to_string(), Box::new(DoubleOperator));
// Now use it in rules
let rule = json!({ "double": 21 });
let compiled = engine.compile(&rule).unwrap();
let result = engine.evaluate_owned(&compiled, json!({})).unwrap();
assert_eq!(result, json!(42.0));
}
Important: Evaluating Arguments
Arguments passed to custom operators are unevaluated. You must call evaluator.evaluate() to resolve them:
#![allow(unused)]
fn main() {
impl Operator for MyOperator {
fn evaluate(
&self,
args: &[Value],
context: &mut ContextStack,
evaluator: &dyn Evaluator,
) -> Result<Value> {
// WRONG: Using args directly
// let value = args[0].as_f64();
// CORRECT: Evaluate first
let value = evaluator.evaluate(&args[0], context)?;
let num = value.as_f64();
// Now work with the evaluated value
// ...
}
}
}
This allows your operator to work with both literals and expressions:
// Works with literals
{ "double": 21 }
// Also works with variables
{ "double": { "var": "x" } }
// And nested expressions
{ "double": { "+": [10, 5] } }
Example: Average Operator
An operator that calculates the average of numbers:
#![allow(unused)]
fn main() {
struct AverageOperator;
impl Operator for AverageOperator {
fn evaluate(
&self,
args: &[Value],
context: &mut ContextStack,
evaluator: &dyn Evaluator,
) -> Result<Value> {
// Evaluate the argument (should be an array)
let value = evaluator.evaluate(
args.first().unwrap_or(&Value::Null),
context
)?;
let arr = value.as_array()
.ok_or_else(|| Error::InvalidArguments("Expected array".to_string()))?;
if arr.is_empty() {
return Ok(Value::Null);
}
let sum: f64 = arr.iter()
.filter_map(|v| v.as_f64())
.sum();
let count = arr.len() as f64;
Ok(json!(sum / count))
}
}
// Usage
engine.add_operator("avg".to_string(), Box::new(AverageOperator));
let rule = json!({ "avg": { "var": "scores" } });
let compiled = engine.compile(&rule).unwrap();
let result = engine.evaluate_owned(&compiled, json!({
"scores": [80, 90, 85, 95]
})).unwrap();
assert_eq!(result, json!(87.5));
}
Example: Range Check Operator
An operator that checks if a value is within a range:
#![allow(unused)]
fn main() {
struct InRangeOperator;
impl Operator for InRangeOperator {
fn evaluate(
&self,
args: &[Value],
context: &mut ContextStack,
evaluator: &dyn Evaluator,
) -> Result<Value> {
if args.len() != 3 {
return Err(Error::InvalidArguments(
"inRange requires 3 arguments: value, min, max".to_string()
));
}
let value = evaluator.evaluate(&args[0], context)?
.as_f64()
.ok_or_else(|| Error::InvalidArguments("Expected number".to_string()))?;
let min = evaluator.evaluate(&args[1], context)?
.as_f64()
.ok_or_else(|| Error::InvalidArguments("Expected number".to_string()))?;
let max = evaluator.evaluate(&args[2], context)?
.as_f64()
.ok_or_else(|| Error::InvalidArguments("Expected number".to_string()))?;
Ok(json!(value >= min && value <= max))
}
}
// Usage
engine.add_operator("inRange".to_string(), Box::new(InRangeOperator));
let rule = json!({ "inRange": [{ "var": "age" }, 18, 65] });
}
Example: String Formatting Operator
#![allow(unused)]
fn main() {
struct FormatOperator;
impl Operator for FormatOperator {
fn evaluate(
&self,
args: &[Value],
context: &mut ContextStack,
evaluator: &dyn Evaluator,
) -> Result<Value> {
let template = evaluator.evaluate(
args.first().unwrap_or(&Value::Null),
context
)?;
let template_str = template.as_str()
.ok_or_else(|| Error::InvalidArguments("Expected string template".to_string()))?;
// Replace {0}, {1}, etc. with arguments
let mut result = template_str.to_string();
for (i, arg) in args.iter().skip(1).enumerate() {
let value = evaluator.evaluate(arg, context)?;
let value_str = match &value {
Value::String(s) => s.clone(),
v => v.to_string(),
};
result = result.replace(&format!("{{{}}}", i), &value_str);
}
Ok(json!(result))
}
}
// Usage
engine.add_operator("format".to_string(), Box::new(FormatOperator));
let rule = json!({
"format": ["Hello, {0}! You have {1} messages.", { "var": "name" }, { "var": "count" }]
});
// Data: { "name": "Alice", "count": 5 }
// Result: "Hello, Alice! You have 5 messages."
}
Thread Safety Requirements
Custom operators must be Send + Sync for thread-safe usage:
#![allow(unused)]
fn main() {
// This is automatically satisfied for most operators
struct MyOperator {
// Use Arc for shared state
config: Arc<Config>,
}
// For mutable state, use synchronization primitives
struct StatefulOperator {
counter: Arc<AtomicUsize>,
}
impl Operator for StatefulOperator {
fn evaluate(
&self,
args: &[Value],
context: &mut ContextStack,
evaluator: &dyn Evaluator,
) -> Result<Value> {
let count = self.counter.fetch_add(1, Ordering::SeqCst);
Ok(json!(count))
}
}
}
Error Handling
Return appropriate errors for invalid inputs:
#![allow(unused)]
fn main() {
impl Operator for MyOperator {
fn evaluate(
&self,
args: &[Value],
context: &mut ContextStack,
evaluator: &dyn Evaluator,
) -> Result<Value> {
// Check argument count
if args.is_empty() {
return Err(Error::InvalidArguments(
"myop requires at least one argument".to_string()
));
}
// Check argument types
let value = evaluator.evaluate(&args[0], context)?;
let num = value.as_f64().ok_or_else(|| {
Error::InvalidArguments(format!(
"Expected number, got {}",
value_type_name(&value)
))
})?;
// Business logic errors
if num < 0.0 {
return Err(Error::Custom(
"Value must be non-negative".to_string()
));
}
Ok(json!(num.sqrt()))
}
}
fn value_type_name(v: &Value) -> &'static str {
match v {
Value::Null => "null",
Value::Bool(_) => "boolean",
Value::Number(_) => "number",
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
}
}
Best Practices
- Always evaluate arguments before using them
- Validate argument count and types early
- Return meaningful error messages
- Keep operators focused - one responsibility per operator
- Document the expected syntax for each operator
- Use
Arcfor shared configuration to maintain thread safety - Test with both literals and expressions as arguments
Configuration
Customize evaluation behavior with EvaluationConfig.
Creating a Configured Engine
#![allow(unused)]
fn main() {
use datalogic_rs::{DataLogic, EvaluationConfig, NanHandling};
// Default configuration
let engine = DataLogic::new();
// Custom configuration
let config = EvaluationConfig::default()
.with_nan_handling(NanHandling::IgnoreValue);
let engine = DataLogic::with_config(config);
}
Configuration Options
NaN Handling
Control how non-numeric values are handled in arithmetic operations.
#![allow(unused)]
fn main() {
use datalogic_rs::{EvaluationConfig, NanHandling};
// Option 1: Throw an error (default)
let config = EvaluationConfig::default()
.with_nan_handling(NanHandling::ThrowError);
// Option 2: Ignore non-numeric values
let config = EvaluationConfig::default()
.with_nan_handling(NanHandling::IgnoreValue);
// Option 3: Coerce to zero
let config = EvaluationConfig::default()
.with_nan_handling(NanHandling::CoerceToZero);
}
Behavior comparison:
#![allow(unused)]
fn main() {
let rule = json!({ "+": [1, "text", 2] });
// NanHandling::ThrowError
// Result: Error
// NanHandling::IgnoreValue
// Result: 3 (ignores "text")
// NanHandling::CoerceToZero
// Result: 3 ("text" becomes 0)
}
Division by Zero
Control how division by zero is handled.
#![allow(unused)]
fn main() {
use datalogic_rs::{EvaluationConfig, DivisionByZero};
// Option 1: Return Infinity/-Infinity (default)
let config = EvaluationConfig::default()
.with_division_by_zero(DivisionByZero::ReturnBounds);
// Option 2: Throw an error
let config = EvaluationConfig::default()
.with_division_by_zero(DivisionByZero::ThrowError);
// Option 3: Return null
let config = EvaluationConfig::default()
.with_division_by_zero(DivisionByZero::ReturnNull);
}
Behavior comparison:
#![allow(unused)]
fn main() {
let rule = json!({ "/": [10, 0] });
// DivisionByZero::ReturnBounds
// Result: Infinity
// DivisionByZero::ThrowError
// Result: Error
// DivisionByZero::ReturnNull
// Result: null
}
Truthiness Evaluation
Control how values are evaluated for truthiness in boolean contexts.
#![allow(unused)]
fn main() {
use datalogic_rs::{EvaluationConfig, TruthyEvaluator};
use std::sync::Arc;
// Option 1: JavaScript-style (default)
let config = EvaluationConfig::default()
.with_truthy_evaluator(TruthyEvaluator::JavaScript);
// Option 2: Python-style
let config = EvaluationConfig::default()
.with_truthy_evaluator(TruthyEvaluator::Python);
// Option 3: Strict boolean (only true/false)
let config = EvaluationConfig::default()
.with_truthy_evaluator(TruthyEvaluator::StrictBoolean);
// Option 4: Custom evaluator
let custom = Arc::new(|value: &serde_json::Value| -> bool {
// Custom logic: only positive numbers are truthy
value.as_f64().map_or(false, |n| n > 0.0)
});
let config = EvaluationConfig::default()
.with_truthy_evaluator(TruthyEvaluator::Custom(custom));
}
Truthiness comparison:
| Value | JavaScript | Python | StrictBoolean |
|---|---|---|---|
true | truthy | truthy | truthy |
false | falsy | falsy | falsy |
1 | truthy | truthy | error/falsy |
0 | falsy | falsy | error/falsy |
"" | falsy | falsy | error/falsy |
"0" | truthy | truthy | error/falsy |
[] | falsy | falsy | error/falsy |
[0] | truthy | truthy | error/falsy |
null | falsy | falsy | error/falsy |
Loose Equality Errors
Control whether loose equality (==) throws errors for incompatible types.
#![allow(unused)]
fn main() {
let config = EvaluationConfig::default()
.with_loose_equality_throws_errors(true); // default
// or
let config = EvaluationConfig::default()
.with_loose_equality_throws_errors(false);
}
Configuration Presets
Safe Arithmetic
Ignores invalid values in arithmetic operations:
#![allow(unused)]
fn main() {
let engine = DataLogic::with_config(EvaluationConfig::safe_arithmetic());
// Equivalent to:
let config = EvaluationConfig::default()
.with_nan_handling(NanHandling::IgnoreValue)
.with_division_by_zero(DivisionByZero::ReturnNull);
}
Strict Mode
Throws errors for any type mismatches:
#![allow(unused)]
fn main() {
let engine = DataLogic::with_config(EvaluationConfig::strict());
// Equivalent to:
let config = EvaluationConfig::default()
.with_nan_handling(NanHandling::ThrowError)
.with_division_by_zero(DivisionByZero::ThrowError)
.with_loose_equality_throws_errors(true);
}
Combining with Structure Preservation
Use both configuration and structure preservation:
#![allow(unused)]
fn main() {
let config = EvaluationConfig::default()
.with_nan_handling(NanHandling::CoerceToZero);
let engine = DataLogic::with_config_and_structure(config, true);
}
Configuration Examples
Lenient Data Processing
For processing potentially messy data:
#![allow(unused)]
fn main() {
let config = EvaluationConfig::default()
.with_nan_handling(NanHandling::IgnoreValue)
.with_division_by_zero(DivisionByZero::ReturnNull);
let engine = DataLogic::with_config(config);
// This won't error even with bad data
let rule = json!({ "+": [1, "not a number", null, 2] });
let result = engine.evaluate_json(&rule.to_string(), "{}").unwrap();
// Result: 3 (ignores non-numeric values)
}
Strict Validation
For scenarios requiring precise type handling:
#![allow(unused)]
fn main() {
let config = EvaluationConfig::strict();
let engine = DataLogic::with_config(config);
// This will error on type mismatches
let rule = json!({ "+": [1, "2"] });
let result = engine.evaluate_json(&rule.to_string(), "{}");
// Result: Error (strict mode doesn't coerce "2" to number)
}
Custom Business Logic Truthiness
For domain-specific truth evaluation:
#![allow(unused)]
fn main() {
use std::sync::Arc;
// Only non-empty strings and positive numbers are truthy
let custom_truthy = Arc::new(|value: &serde_json::Value| -> bool {
match value {
serde_json::Value::Bool(b) => *b,
serde_json::Value::Number(n) => n.as_f64().map_or(false, |n| n > 0.0),
serde_json::Value::String(s) => !s.is_empty(),
_ => false,
}
});
let config = EvaluationConfig::default()
.with_truthy_evaluator(TruthyEvaluator::Custom(custom_truthy));
let engine = DataLogic::with_config(config);
// With this config:
// { "if": [0, "yes", "no"] } => "no" (0 is not positive)
// { "if": [-5, "yes", "no"] } => "no" (-5 is not positive)
// { "if": [1, "yes", "no"] } => "yes" (1 is positive)
}
Structured Objects (Templating)
Use JSONLogic as a templating engine with structure preservation mode.
Enabling Structure Preservation
#![allow(unused)]
fn main() {
use datalogic_rs::DataLogic;
// Enable structure preservation
let engine = DataLogic::with_preserve_structure();
// Or combine with configuration
let engine = DataLogic::with_config_and_structure(config, true);
}
How It Works
In normal mode, unknown keys in a JSON object are treated as errors (or custom operators). With structure preservation enabled, unknown keys become literal output fields.
Normal mode:
{ "user": { "var": "name" } }
// Error: "user" is not a known operator
Structure preservation mode:
{ "user": { "var": "name" } }
// Result: { "user": "Alice" }
// "user" is preserved as an output key
Basic Templating
#![allow(unused)]
fn main() {
use datalogic_rs::DataLogic;
use serde_json::json;
let engine = DataLogic::with_preserve_structure();
let template = json!({
"greeting": { "cat": ["Hello, ", { "var": "name" }, "!"] },
"timestamp": { "now": [] },
"isAdmin": { "==": [{ "var": "role" }, "admin"] }
});
let compiled = engine.compile(&template).unwrap();
let result = engine.evaluate_owned(&compiled, json!({
"name": "Alice",
"role": "admin"
})).unwrap();
// Result:
// {
// "greeting": "Hello, Alice!",
// "timestamp": 1704067200000,
// "isAdmin": true
// }
}
Nested Structures
Structure preservation works at any depth:
#![allow(unused)]
fn main() {
let template = json!({
"user": {
"profile": {
"displayName": { "var": "firstName" },
"email": { "var": "userEmail" },
"verified": true
},
"settings": {
"theme": { "??": [{ "var": "preferredTheme" }, "light"] },
"notifications": { "var": "notificationsEnabled" }
}
},
"metadata": {
"generatedAt": { "now": [] },
"version": "1.0"
}
});
let result = engine.evaluate_owned(&compiled, json!({
"firstName": "Bob",
"userEmail": "bob@example.com",
"notificationsEnabled": true
})).unwrap();
// Result:
// {
// "user": {
// "profile": {
// "displayName": "Bob",
// "email": "bob@example.com",
// "verified": true
// },
// "settings": {
// "theme": "light",
// "notifications": true
// }
// },
// "metadata": {
// "generatedAt": 1704067200000,
// "version": "1.0"
// }
// }
}
Arrays in Templates
Arrays are processed element by element:
#![allow(unused)]
fn main() {
let template = json!({
"items": [
{ "name": "Item 1", "price": { "var": "price1" } },
{ "name": "Item 2", "price": { "var": "price2" } }
],
"total": { "+": [{ "var": "price1" }, { "var": "price2" }] }
});
let result = engine.evaluate_owned(&compiled, json!({
"price1": 10,
"price2": 20
})).unwrap();
// Result:
// {
// "items": [
// { "name": "Item 1", "price": 10 },
// { "name": "Item 2", "price": 20 }
// ],
// "total": 30
// }
}
Dynamic Arrays with Map
Generate arrays dynamically using map:
#![allow(unused)]
fn main() {
let template = json!({
"users": {
"map": [
{ "var": "userList" },
{
"id": { "var": "id" },
"name": { "var": "name" },
"isActive": { "var": "active" }
}
]
}
});
let result = engine.evaluate_owned(&compiled, json!({
"userList": [
{ "id": 1, "name": "Alice", "active": true },
{ "id": 2, "name": "Bob", "active": false }
]
})).unwrap();
// Result:
// {
// "users": [
// { "id": 1, "name": "Alice", "isActive": true },
// { "id": 2, "name": "Bob", "isActive": false }
// ]
// }
}
The preserve Operator
Use the preserve operator to explicitly preserve a value without evaluation:
#![allow(unused)]
fn main() {
let template = json!({
"data": { "var": "input" },
"literal": { "preserve": { "var": "this is literal" } }
});
// Result:
// {
// "data": <value of input>,
// "literal": { "var": "this is literal" }
// }
}
Use Cases
API Response Transformation
#![allow(unused)]
fn main() {
let template = json!({
"success": true,
"data": {
"user": {
"id": { "var": "userId" },
"profile": {
"name": { "cat": [{ "var": "firstName" }, " ", { "var": "lastName" }] },
"avatar": { "cat": ["https://cdn.example.com/", { "var": "avatarId" }, ".jpg"] }
}
}
},
"timestamp": { "now": [] }
});
}
Document Generation
#![allow(unused)]
fn main() {
let template = json!({
"invoice": {
"number": { "cat": ["INV-", { "var": "invoiceId" }] },
"date": { "format_date": [{ "now": [] }, "%Y-%m-%d"] },
"customer": {
"name": { "var": "customerName" },
"address": { "var": "customerAddress" }
},
"items": { "var": "lineItems" },
"total": {
"reduce": [
{ "var": "lineItems" },
{ "+": [{ "var": "accumulator" }, { "var": "current.amount" }] },
0
]
}
}
});
}
Configuration Templating
#![allow(unused)]
fn main() {
let template = json!({
"database": {
"host": { "??": [{ "var": "DB_HOST" }, "localhost"] },
"port": { "??": [{ "var": "DB_PORT" }, 5432] },
"name": { "var": "DB_NAME" },
"ssl": { "==": [{ "var": "ENV" }, "production"] }
},
"cache": {
"enabled": { "var": "CACHE_ENABLED" },
"ttl": { "if": [
{ "==": [{ "var": "ENV" }, "development"] },
60,
3600
]}
}
});
}
Dynamic Forms
#![allow(unused)]
fn main() {
let template = json!({
"form": {
"title": { "var": "formTitle" },
"fields": {
"map": [
{ "var": "fieldDefinitions" },
{
"name": { "var": "name" },
"type": { "var": "type" },
"required": { "var": "required" },
"label": { "cat": [{ "var": "name" }, { "if": [{ "var": "required" }, " *", ""] }] }
}
]
}
}
});
}
Mixing Operators and Structure
You can mix operators and structure freely:
#![allow(unused)]
fn main() {
let template = json!({
// Static structure
"type": "response",
"version": "2.0",
// Conditional structure
"status": { "if": [
{ "var": "success" },
"ok",
"error"
]},
// Dynamic content
"data": { "if": [
{ "var": "success" },
{
"result": { "var": "data" },
"count": { "length": { "var": "data" } }
},
{
"error": { "var": "errorMessage" },
"code": { "var": "errorCode" }
}
]}
});
}
Thread Safety
datalogic-rs is designed for thread-safe, concurrent evaluation.
Thread-Safe Design
CompiledLogic is Arc-wrapped
When you compile a rule, it’s automatically wrapped in Arc:
#![allow(unused)]
fn main() {
use datalogic_rs::DataLogic;
use serde_json::json;
let engine = DataLogic::new();
let rule = json!({ ">": [{ "var": "x" }, 10] });
// compiled is Arc<CompiledLogic>
let compiled = engine.compile(&rule).unwrap();
// Clone is cheap (just increments reference count)
let compiled_clone = compiled.clone(); // or Arc::clone(&compiled)
}
Sharing Across Threads
#![allow(unused)]
fn main() {
use datalogic_rs::DataLogic;
use serde_json::json;
use std::sync::Arc;
use std::thread;
let engine = Arc::new(DataLogic::new());
let rule = json!({ "*": [{ "var": "x" }, 2] });
let compiled = engine.compile(&rule).unwrap();
let handles: Vec<_> = (0..4).map(|i| {
let engine = Arc::clone(&engine);
let compiled = Arc::clone(&compiled);
thread::spawn(move || {
let result = engine.evaluate_owned(
&compiled,
json!({ "x": i })
).unwrap();
println!("Thread {}: {}", i, result);
})
}).collect();
for handle in handles {
handle.join().unwrap();
}
}
Async Runtime Integration
With Tokio
use datalogic_rs::DataLogic;
use serde_json::json;
use std::sync::Arc;
#[tokio::main]
async fn main() {
let engine = Arc::new(DataLogic::new());
let rule = json!({ "+": [{ "var": "a" }, { "var": "b" }] });
let compiled = engine.compile(&rule).unwrap();
// Spawn multiple async tasks
let tasks: Vec<_> = (0..10).map(|i| {
let engine = Arc::clone(&engine);
let compiled = Arc::clone(&compiled);
tokio::spawn(async move {
// Use spawn_blocking for CPU-bound evaluation
tokio::task::spawn_blocking(move || {
engine.evaluate_owned(&compiled, json!({ "a": i, "b": i * 2 }))
}).await.unwrap()
})
}).collect();
for task in tasks {
let result = task.await.unwrap().unwrap();
println!("Result: {}", result);
}
}
Evaluation is CPU-bound
Since evaluation is CPU-bound (not I/O), use spawn_blocking in async contexts:
#![allow(unused)]
fn main() {
async fn evaluate_rule(
engine: Arc<DataLogic>,
compiled: Arc<CompiledLogic>,
data: Value,
) -> Result<Value, Error> {
tokio::task::spawn_blocking(move || {
engine.evaluate_owned(&compiled, data)
}).await.unwrap()
}
}
Thread Pool Pattern
For high-throughput scenarios, use a thread pool:
#![allow(unused)]
fn main() {
use datalogic_rs::DataLogic;
use serde_json::json;
use std::sync::Arc;
use rayon::prelude::*;
let engine = Arc::new(DataLogic::new());
let rule = json!({ "filter": [
{ "var": "items" },
{ ">": [{ "var": "value" }, 50] }
]});
let compiled = engine.compile(&rule).unwrap();
// Process many data sets in parallel
let datasets: Vec<Value> = (0..1000)
.map(|i| json!({
"items": (0..100).map(|j| json!({ "value": (i + j) % 100 })).collect::<Vec<_>>()
}))
.collect();
let results: Vec<_> = datasets
.par_iter() // Rayon parallel iterator
.map(|data| {
engine.evaluate(&compiled, data).unwrap()
})
.collect();
}
Shared Engine vs Per-Thread Engine
Shared Engine (Recommended)
Share one engine across threads when using the same custom operators:
#![allow(unused)]
fn main() {
use std::sync::Arc;
let mut engine = DataLogic::new();
engine.add_operator("custom".to_string(), Box::new(MyOperator));
let engine = Arc::new(engine);
// Share across threads
for _ in 0..4 {
let engine = Arc::clone(&engine);
thread::spawn(move || {
// Use shared engine
});
}
}
Per-Thread Engine
Create separate engines when you need thread-local state:
#![allow(unused)]
fn main() {
thread_local! {
static ENGINE: DataLogic = {
let mut engine = DataLogic::new();
// Thread-local configuration
engine
};
}
// Use in each thread
ENGINE.with(|engine| {
let compiled = engine.compile(&rule).unwrap();
engine.evaluate_owned(&compiled, data)
});
}
Custom Operator Thread Safety
Custom operators must implement Send + Sync:
#![allow(unused)]
fn main() {
use std::sync::{Arc, atomic::{AtomicUsize, Ordering}};
// Thread-safe custom operator
struct CounterOperator {
counter: Arc<AtomicUsize>,
}
impl Operator for CounterOperator {
fn evaluate(
&self,
args: &[Value],
context: &mut ContextStack,
evaluator: &dyn Evaluator,
) -> Result<Value> {
let count = self.counter.fetch_add(1, Ordering::SeqCst);
Ok(json!(count))
}
}
// Create shared counter
let counter = Arc::new(AtomicUsize::new(0));
let mut engine = DataLogic::new();
engine.add_operator("count".to_string(), Box::new(CounterOperator {
counter: Arc::clone(&counter),
}));
}
Performance Considerations
Compile Once, Evaluate Many
#![allow(unused)]
fn main() {
// GOOD: Compile once
let compiled = engine.compile(&rule).unwrap();
for data in datasets {
engine.evaluate(&compiled, &data);
}
// BAD: Compiling in a loop
for data in datasets {
let compiled = engine.compile(&rule).unwrap(); // Unnecessary!
engine.evaluate(&compiled, &data);
}
}
Minimize Cloning
#![allow(unused)]
fn main() {
// GOOD: Use references where possible
let result = engine.evaluate(&compiled, &data)?;
// Use owned version only when you don't need the data afterwards
let result = engine.evaluate_owned(&compiled, data)?;
}
Batch Processing
#![allow(unused)]
fn main() {
// Process in batches to balance parallelism overhead
let batch_size = 100;
for chunk in datasets.chunks(batch_size) {
let results: Vec<_> = chunk.par_iter()
.map(|data| engine.evaluate(&compiled, data))
.collect();
// Process results
}
}
Error Handling in Threads
#![allow(unused)]
fn main() {
use std::thread;
let handles: Vec<_> = datasets.into_iter().map(|data| {
let engine = Arc::clone(&engine);
let compiled = Arc::clone(&compiled);
thread::spawn(move || -> Result<Value, Error> {
engine.evaluate_owned(&compiled, data)
})
}).collect();
// Collect results, handling errors
let results: Vec<Result<Value, Error>> = handles
.into_iter()
.map(|h| h.join().expect("Thread panicked"))
.collect();
// Process results
for (i, result) in results.into_iter().enumerate() {
match result {
Ok(value) => println!("Result {}: {}", i, value),
Err(e) => eprintln!("Error {}: {}", i, e),
}
}
}
API Reference
Core types and methods in datalogic-rs.
DataLogic
The main engine for compiling and evaluating JSONLogic rules.
Creating an Engine
#![allow(unused)]
fn main() {
use datalogic_rs::DataLogic;
// Default engine
let engine = DataLogic::new();
// With configuration
let engine = DataLogic::with_config(config);
// With structure preservation (templating mode)
let engine = DataLogic::with_preserve_structure();
// With both
let engine = DataLogic::with_config_and_structure(config, true);
}
Methods
compile
Compile a JSONLogic rule into an optimized representation.
#![allow(unused)]
fn main() {
pub fn compile(&self, logic: &Value) -> Result<Arc<CompiledLogic>>
}
Parameters:
logic- The JSONLogic rule as aserde_json::Value
Returns:
Ok(Arc<CompiledLogic>)- Compiled rule, thread-safe and shareableErr(Error)- Compilation error
Example:
#![allow(unused)]
fn main() {
let rule = json!({ ">": [{ "var": "x" }, 10] });
let compiled = engine.compile(&rule)?;
}
evaluate
Evaluate compiled logic against borrowed data.
#![allow(unused)]
fn main() {
pub fn evaluate(&self, compiled: &CompiledLogic, data: &Value) -> Result<Value>
}
Parameters:
compiled- Reference to compiled logicdata- Reference to input data
Returns:
Ok(Value)- Evaluation resultErr(Error)- Evaluation error
Example:
#![allow(unused)]
fn main() {
let data = json!({ "x": 15 });
let result = engine.evaluate(&compiled, &data)?;
}
evaluate_owned
Evaluate compiled logic, taking ownership of data.
#![allow(unused)]
fn main() {
pub fn evaluate_owned(&self, compiled: &CompiledLogic, data: Value) -> Result<Value>
}
Parameters:
compiled- Reference to compiled logicdata- Input data (owned)
Returns:
Ok(Value)- Evaluation resultErr(Error)- Evaluation error
Example:
#![allow(unused)]
fn main() {
let result = engine.evaluate_owned(&compiled, json!({ "x": 15 }))?;
}
evaluate_json
Convenience method to evaluate JSON strings directly.
#![allow(unused)]
fn main() {
pub fn evaluate_json(&self, logic: &str, data: &str) -> Result<Value>
}
Parameters:
logic- JSONLogic rule as a JSON stringdata- Input data as a JSON string
Returns:
Ok(Value)- Evaluation resultErr(Error)- Parse or evaluation error
Example:
#![allow(unused)]
fn main() {
let result = engine.evaluate_json(
r#"{ "+": [1, 2] }"#,
r#"{}"#
)?;
}
add_operator
Register a custom operator.
#![allow(unused)]
fn main() {
pub fn add_operator(&mut self, name: String, operator: Box<dyn Operator>)
}
Parameters:
name- Operator name (used in rules)operator- Boxed operator implementation
Example:
#![allow(unused)]
fn main() {
engine.add_operator("double".to_string(), Box::new(DoubleOperator));
}
CompiledLogic
Pre-compiled rule representation. Created by DataLogic::compile().
Characteristics
- Thread-safe: Wrapped in
Arc, implementsSend + Sync - Immutable: Cannot be modified after compilation
- Shareable: Cheap to clone (reference counting)
Usage
#![allow(unused)]
fn main() {
// Compile once
let compiled = engine.compile(&rule)?;
// Share across threads
let compiled_clone = Arc::clone(&compiled);
std::thread::spawn(move || {
engine.evaluate(&compiled_clone, &data);
});
// Evaluate multiple times
for data in datasets {
engine.evaluate(&compiled, &data)?;
}
}
EvaluationConfig
Configuration for evaluation behavior.
Creating Configuration
#![allow(unused)]
fn main() {
use datalogic_rs::EvaluationConfig;
let config = EvaluationConfig::default();
}
Builder Methods
#![allow(unused)]
fn main() {
// NaN handling
config.with_nan_handling(NanHandling::IgnoreValue)
// Division by zero
config.with_division_by_zero(DivisionByZero::ReturnNull)
// Truthiness evaluation
config.with_truthy_evaluator(TruthyEvaluator::Python)
// Loose equality errors
config.with_loose_equality_throws_errors(false)
}
Presets
#![allow(unused)]
fn main() {
// Lenient arithmetic
let config = EvaluationConfig::safe_arithmetic();
// Strict type checking
let config = EvaluationConfig::strict();
}
NanHandling
How to handle non-numeric values in arithmetic.
#![allow(unused)]
fn main() {
pub enum NanHandling {
ThrowError, // Default: throw an error
IgnoreValue, // Skip non-numeric values
CoerceToZero, // Treat as 0
}
}
DivisionByZero
How to handle division by zero.
#![allow(unused)]
fn main() {
pub enum DivisionByZero {
ReturnBounds, // Default: return Infinity/-Infinity
ThrowError, // Throw an error
ReturnNull, // Return null
}
}
TruthyEvaluator
How to evaluate truthiness.
#![allow(unused)]
fn main() {
pub enum TruthyEvaluator {
JavaScript, // Default: JS-style truthiness
Python, // Python-style truthiness
StrictBoolean, // Only true/false are valid
Custom(Arc<dyn Fn(&Value) -> bool + Send + Sync>),
}
}
Operator Trait
Interface for custom operators.
#![allow(unused)]
fn main() {
pub trait Operator: Send + Sync {
fn evaluate(
&self,
args: &[Value],
context: &mut ContextStack,
evaluator: &dyn Evaluator,
) -> Result<Value>;
}
}
Parameters:
args- Unevaluated arguments from the rulecontext- Current evaluation contextevaluator- Interface to evaluate nested expressions
Important: Arguments are unevaluated. Call evaluator.evaluate() to resolve them.
Evaluator Trait
Interface for evaluating expressions (used in custom operators).
#![allow(unused)]
fn main() {
pub trait Evaluator {
fn evaluate(&self, value: &Value, context: &mut ContextStack) -> Result<Value>;
}
}
ContextStack
Manages variable scope during evaluation.
Accessing Current Element
In array operations (map, filter, reduce):
#![allow(unused)]
fn main() {
// Access current element
let current = context.current();
// Access current index
let index = context.index();
// Access accumulator (in reduce)
let acc = context.accumulator();
}
Error
Error types returned by datalogic-rs.
#![allow(unused)]
fn main() {
pub enum Error {
InvalidArguments(String),
UnknownOperator(String),
TypeError(String),
DivisionByZero,
Custom(String),
// ... other variants
}
}
Common Error Handling
#![allow(unused)]
fn main() {
use datalogic_rs::Error;
match engine.evaluate(&compiled, &data) {
Ok(result) => println!("Result: {}", result),
Err(Error::InvalidArguments(msg)) => eprintln!("Bad arguments: {}", msg),
Err(Error::UnknownOperator(op)) => eprintln!("Unknown operator: {}", op),
Err(e) => eprintln!("Error: {}", e),
}
}
Result Type
#![allow(unused)]
fn main() {
pub type Result<T> = std::result::Result<T, Error>;
}
Full Example
use datalogic_rs::{DataLogic, EvaluationConfig, NanHandling, Error};
use serde_json::json;
use std::sync::Arc;
fn main() -> Result<(), Error> {
// Create configured engine
let config = EvaluationConfig::default()
.with_nan_handling(NanHandling::IgnoreValue);
let engine = Arc::new(DataLogic::with_config(config));
// Compile rule
let rule = json!({
"if": [
{ ">=": [{ "var": "score" }, 60] },
"pass",
"fail"
]
});
let compiled = engine.compile(&rule)?;
// Evaluate with different data
let results: Vec<_> = vec![
json!({ "score": 85 }),
json!({ "score": 45 }),
json!({ "score": 60 }),
].into_iter()
.map(|data| engine.evaluate_owned(&compiled, data))
.collect();
for result in results {
println!("{}", result?);
}
Ok(())
}
Use Cases & Examples
Real-world examples of using datalogic-rs for common scenarios.
Feature Flags
Control feature availability based on user attributes.
Basic Feature Flag
#![allow(unused)]
fn main() {
use datalogic_rs::DataLogic;
use serde_json::json;
let engine = DataLogic::new();
// Feature available to premium users in US
let rule = json!({
"and": [
{ "==": [{ "var": "user.plan" }, "premium"] },
{ "==": [{ "var": "user.country" }, "US"] }
]
});
let compiled = engine.compile(&rule).unwrap();
let user_data = json!({
"user": {
"plan": "premium",
"country": "US"
}
});
let enabled = engine.evaluate_owned(&compiled, user_data).unwrap();
assert_eq!(enabled, json!(true));
}
Percentage Rollout
#![allow(unused)]
fn main() {
// Enable for 20% of users (based on user ID hash)
let rule = json!({
"<": [
{ "%": [{ "var": "user.id" }, 100] },
20
]
});
}
Beta Access
#![allow(unused)]
fn main() {
// Enable for beta testers OR employees OR users who signed up before a date
let rule = json!({
"or": [
{ "==": [{ "var": "user.role" }, "beta_tester"] },
{ "ends_with": [{ "var": "user.email" }, "@company.com"] },
{ "<": [{ "var": "user.signup_date" }, "2024-01-01"] }
]
});
}
Dynamic Pricing
Calculate prices based on rules.
Discount by Quantity
#![allow(unused)]
fn main() {
let rule = json!({
"if": [
{ ">=": [{ "var": "quantity" }, 100] },
{ "*": [{ "var": "base_price" }, 0.8] }, // 20% off
{ "if": [
{ ">=": [{ "var": "quantity" }, 50] },
{ "*": [{ "var": "base_price" }, 0.9] }, // 10% off
{ "var": "base_price" }
]}
]
});
let data = json!({
"quantity": 75,
"base_price": 100
});
let price = engine.evaluate_owned(&compiled, data).unwrap();
// Result: 90 (10% discount)
}
Tiered Pricing
#![allow(unused)]
fn main() {
let rule = json!({
"+": [
// First 10 units at $10
{ "*": [{ "min": [{ "var": "quantity" }, 10] }, 10] },
// Next 40 units at $8
{ "*": [
{ "max": [{ "-": [{ "min": [{ "var": "quantity" }, 50] }, 10] }, 0] },
8
]},
// Remaining units at $6
{ "*": [
{ "max": [{ "-": [{ "var": "quantity" }, 50] }, 0] },
6
]}
]
});
}
Member Pricing
#![allow(unused)]
fn main() {
let rule = json!({
"if": [
{ "var": "user.is_member" },
{ "*": [
{ "var": "product.price" },
{ "-": [1, { "/": [{ "var": "user.member_discount" }, 100] }] }
]},
{ "var": "product.price" }
]
});
let data = json!({
"user": { "is_member": true, "member_discount": 15 },
"product": { "price": 200 }
});
// Result: 170 (15% member discount)
}
Form Validation
Validate user input with complex rules.
Required Fields
#![allow(unused)]
fn main() {
let rule = json!({
"if": [
{ "missing": ["name", "email", "password"] },
{
"valid": false,
"errors": { "missing": ["name", "email", "password"] }
},
{ "valid": true }
]
});
}
Field Constraints
#![allow(unused)]
fn main() {
let engine = DataLogic::with_preserve_structure();
let rule = json!({
"valid": { "and": [
// Email format
{ "in": ["@", { "var": "email" }] },
// Password length
{ ">=": [{ "length": { "var": "password" } }, 8] },
// Age range
{ "and": [
{ ">=": [{ "var": "age" }, 18] },
{ "<=": [{ "var": "age" }, 120] }
]}
]},
"errors": { "filter": [
[
{ "if": [
{ "!": { "in": ["@", { "var": "email" }] } },
"Invalid email format",
null
]},
{ "if": [
{ "<": [{ "length": { "var": "password" } }, 8] },
"Password must be at least 8 characters",
null
]},
{ "if": [
{ "or": [
{ "<": [{ "var": "age" }, 18] },
{ ">": [{ "var": "age" }, 120] }
]},
"Age must be between 18 and 120",
null
]}
],
{ "!==": [{ "var": "" }, null] }
]}
});
}
Conditional Validation
#![allow(unused)]
fn main() {
// If business account, require company name
let rule = json!({
"if": [
{ "and": [
{ "==": [{ "var": "account_type" }, "business"] },
{ "missing": ["company_name"] }
]},
{ "error": "Company name required for business accounts" },
{ "valid": true }
]
});
}
Access Control
Determine user permissions.
Role-Based Access
#![allow(unused)]
fn main() {
let rule = json!({
"or": [
{ "==": [{ "var": "user.role" }, "admin"] },
{ "and": [
{ "==": [{ "var": "user.role" }, "editor"] },
{ "==": [{ "var": "resource.owner_id" }, { "var": "user.id" }] }
]}
]
});
}
Permission Checking
#![allow(unused)]
fn main() {
let rule = json!({
"in": [
{ "var": "required_permission" },
{ "var": "user.permissions" }
]
});
let data = json!({
"user": {
"permissions": ["read", "write", "delete"]
},
"required_permission": "write"
});
// Result: true
}
Time-Based Access
#![allow(unused)]
fn main() {
let rule = json!({
"and": [
// Has permission
{ "in": ["access_data", { "var": "user.permissions" }] },
// Within allowed hours (9 AM - 6 PM)
{ "and": [
{ ">=": [{ "var": "current_hour" }, 9] },
{ "<": [{ "var": "current_hour" }, 18] }
]},
// On a weekday
{ "in": [{ "var": "current_day" }, [1, 2, 3, 4, 5]] }
]
});
}
Fraud Detection
Score and flag potentially fraudulent transactions.
Risk Scoring
#![allow(unused)]
fn main() {
let rule = json!({
"+": [
// High amount
{ "if": [{ ">": [{ "var": "amount" }, 1000] }, 30, 0] },
// New account
{ "if": [{ "<": [{ "var": "account_age_days" }, 7] }, 25, 0] },
// Different country
{ "if": [
{ "!=": [{ "var": "billing_country" }, { "var": "shipping_country" }] },
20,
0
]},
// Multiple attempts
{ "if": [{ ">": [{ "var": "attempts_last_hour" }, 3] }, 25, 0] },
// Unusual time
{ "if": [
{ "or": [
{ "<": [{ "var": "hour" }, 6] },
{ ">": [{ "var": "hour" }, 23] }
]},
15,
0
]}
]
});
// Score > 50 = flag for review
let data = json!({
"amount": 1500,
"account_age_days": 3,
"billing_country": "US",
"shipping_country": "CA",
"attempts_last_hour": 1,
"hour": 14
});
// Result: 75 (high amount + new account + different country)
}
Velocity Checks
#![allow(unused)]
fn main() {
let rule = json!({
"or": [
// Too many transactions in short time
{ ">": [{ "var": "transactions_last_hour" }, 10] },
// Too much total amount
{ ">": [{ "var": "total_amount_last_hour" }, 5000] },
// Same card used from multiple IPs
{ ">": [{ "var": "unique_ips_last_day" }, 3] }
]
});
}
Data Transformation
Transform and reshape data.
API Response Mapping
#![allow(unused)]
fn main() {
let engine = DataLogic::with_preserve_structure();
let template = json!({
"users": {
"map": [
{ "var": "raw_users" },
{
"id": { "var": "user_id" },
"fullName": { "cat": [{ "var": "first_name" }, " ", { "var": "last_name" }] },
"email": { "lower": { "var": "email" } },
"isActive": { "==": [{ "var": "status" }, "active"] }
}
]
},
"total": { "length": { "var": "raw_users" } },
"activeCount": { "length": {
"filter": [
{ "var": "raw_users" },
{ "==": [{ "var": "status" }, "active"] }
]
}}
});
}
Report Generation
#![allow(unused)]
fn main() {
let template = json!({
"report": {
"title": { "cat": ["Sales Report - ", { "var": "period" }] },
"generated": { "format_date": [{ "now": [] }, "%Y-%m-%d %H:%M"] },
"summary": {
"totalSales": { "reduce": [
{ "var": "transactions" },
{ "+": [{ "var": "accumulator" }, { "var": "current.amount" }] },
0
]},
"avgTransaction": { "/": [
{ "reduce": [
{ "var": "transactions" },
{ "+": [{ "var": "accumulator" }, { "var": "current.amount" }] },
0
]},
{ "length": { "var": "transactions" } }
]},
"topCategory": { "var": "top_category" }
}
}
});
}
Notification Rules
Determine when and how to send notifications.
Alert Conditions
#![allow(unused)]
fn main() {
let rule = json!({
"if": [
// Critical: immediate
{ ">": [{ "var": "error_rate" }, 10] },
{ "channel": "pager", "priority": "critical" },
// Warning: Slack
{ "if": [
{ ">": [{ "var": "error_rate" }, 5] },
{ "channel": "slack", "priority": "warning" },
// Info: email digest
{ "if": [
{ ">": [{ "var": "error_rate" }, 1] },
{ "channel": "email", "priority": "info" },
null
]}
]}
]
});
}
User Preferences
#![allow(unused)]
fn main() {
let rule = json!({
"and": [
// User has enabled notifications
{ "var": "user.notifications_enabled" },
// Notification type is in user's preferences
{ "in": [
{ "var": "notification.type" },
{ "var": "user.enabled_types" }
]},
// Within user's quiet hours
{ "!": { "and": [
{ ">=": [{ "var": "current_hour" }, { "var": "user.quiet_start" }] },
{ "<": [{ "var": "current_hour" }, { "var": "user.quiet_end" }] }
]}}
]
});
}
Performance
This guide covers performance optimization, benchmarking, and best practices for datalogic-rs.
Performance Characteristics
Compilation vs Evaluation
datalogic-rs uses a two-phase approach:
- Compilation (slower): Parse and optimize the JSONLogic expression
- Evaluation (faster): Execute compiled logic against data
Best practice: Compile once, evaluate many times.
#![allow(unused)]
fn main() {
// Compile once
let compiled = engine.compile(&logic)?;
// Evaluate many times
for data in dataset {
engine.evaluate_owned(&compiled, data)?;
}
}
OpCode Dispatch
Built-in operators use direct OpCode dispatch instead of string lookups:
- 59 built-in operators have direct dispatch
- Custom operators use map lookup (still fast)
- No runtime reflection or dynamic dispatch
Memory Efficiency
v4 optimizations:
SmallVecfor small arrays (avoids heap allocation)Cowtypes for value passing (avoids cloning)Arcfor compiled logic (cheap cloning for threads)
Benchmarking
Running Benchmarks
# Run the benchmark example
cargo run --example benchmark --release
# With custom iterations
BENCH_ITERATIONS=100000 cargo run --example benchmark --release
Sample Results
Typical performance on modern hardware:
| Operation | Time |
|---|---|
| Simple comparison | ~50ns |
| Variable access | ~100ns |
| Complex nested logic | ~500ns |
| Array map (10 items) | ~2μs |
| Large expression (50+ nodes) | ~10μs |
Results vary by CPU, expression complexity, and data size.
Creating Custom Benchmarks
use std::time::Instant;
use datalogic_rs::DataLogic;
use serde_json::json;
fn main() {
let engine = DataLogic::new();
let logic = json!({ "==": [{ "var": "x" }, 1] });
let compiled = engine.compile(&logic).unwrap();
let data = json!({ "x": 1 });
let iterations = 100_000;
let start = Instant::now();
for _ in 0..iterations {
let _ = engine.evaluate_owned(&compiled, data.clone());
}
let elapsed = start.elapsed();
let per_op = elapsed / iterations;
println!("Time per evaluation: {:?}", per_op);
}
Optimization Tips
1. Reuse Compiled Rules
Bad:
#![allow(unused)]
fn main() {
for item in items {
let compiled = engine.compile(&logic)?; // Recompiles every time!
engine.evaluate_owned(&compiled, item)?;
}
}
Good:
#![allow(unused)]
fn main() {
let compiled = engine.compile(&logic)?;
for item in items {
engine.evaluate_owned(&compiled, item)?;
}
}
2. Use References for Large Data
#![allow(unused)]
fn main() {
// Clones data (fine for small data)
engine.evaluate_owned(&compiled, large_data)
// Uses reference (better for large data)
engine.evaluate(&compiled, &large_data)
}
3. Avoid Unnecessary Cloning in Custom Operators
#![allow(unused)]
fn main() {
impl Operator for MyOperator {
fn evaluate(&self, args: &[Value], context: &mut ContextStack, evaluator: &dyn Evaluator) -> Result<Value> {
// Avoid: cloning arguments unnecessarily
// let value = args[0].clone();
// Better: evaluate directly
let value = evaluator.evaluate(&args[0], context)?;
// ...
}
}
}
4. Short-Circuit Evaluation
and and or operators short-circuit. Order conditions by:
- Cheapest to evaluate first
- Most likely to short-circuit first
{
"and": [
{ "var": "isActive" }, // Simple variable check (fast)
{ "in": ["admin", { "var": "roles" }] } // Array search (slower)
]
}
5. Use Specific Operators
Some operators are more efficient than others:
// Less efficient: substring check
{ "in": ["@", { "var": "email" }] }
// More efficient: dedicated operator (when available)
{ "contains": [{ "var": "email" }, "@"] }
6. Minimize Nested Variable Access
Deep nesting requires multiple map lookups:
// Slower: deep nesting
{ "var": "user.profile.settings.theme.color" }
// Faster: flatter structure
{ "var": "themeColor" }
JavaScript/WASM Performance
CompiledRule Advantage
// Benchmark
const iterations = 10000;
// Without CompiledRule
console.time('evaluate');
for (let i = 0; i < iterations; i++) {
evaluate(logic, data, false);
}
console.timeEnd('evaluate');
// With CompiledRule
const rule = new CompiledRule(logic, false);
console.time('compiled');
for (let i = 0; i < iterations; i++) {
rule.evaluate(data);
}
console.timeEnd('compiled');
Typical improvement: 2-5x faster with CompiledRule.
WASM Considerations
- Initialization: Call
init()once at startup - String overhead: JSON.stringify/parse has some cost
- Memory: WASM has its own memory space (efficient for large operations)
React UI Performance
For the DataLogicEditor component:
-
Memoize expressions:
const expression = useMemo(() => ({ ... }), [deps]); -
Debounce data changes in debug mode:
const debouncedData = useDebouncedValue(data, 200); <DataLogicEditor value={expr} data={debouncedData} mode="debug" /> -
Use visualize mode when debugging isn’t needed:
<DataLogicEditor value={expr} mode="visualize" />
Profiling
Rust Profiling
Use standard Rust profiling tools:
# With perf (Linux)
cargo build --release
perf record ./target/release/your-binary
perf report
# With Instruments (macOS)
cargo instruments --release -t "CPU Profiler"
Tracing for Bottlenecks
Use evaluate_with_trace to identify slow sub-expressions:
const trace = evaluate_with_trace(logic, data, false);
const { steps } = JSON.parse(trace);
// Analyze which steps take longest
steps.forEach(step => {
console.log(step.operator, step.duration_ns);
});
Comparison with Other Engines
datalogic-rs is designed for high-throughput evaluation. Compared to:
- json-logic-js (JavaScript): 10-50x faster for complex rules
- json-logic-py (Python): 20-100x faster
- Other Rust implementations: Competitive, with better ergonomics
Actual performance depends on:
- Expression complexity
- Data size
- Evaluation frequency
- Thread utilization
Production Recommendations
- Pre-compile all rules at startup
- Use connection/worker pools for parallel evaluation
- Monitor evaluation latency in production
- Set appropriate timeouts for untrusted rules
- Consider rule complexity limits for user-defined logic
#![allow(unused)]
fn main() {
// Production pattern
use std::sync::Arc;
struct RuleEngine {
engine: Arc<DataLogic>,
rules: HashMap<String, Arc<CompiledLogic>>,
}
impl RuleEngine {
pub fn new() -> Self {
let engine = Arc::new(DataLogic::new());
let mut rules = HashMap::new();
// Pre-compile all rules at startup
for (name, logic) in load_rules() {
let compiled = engine.compile(&logic).unwrap();
rules.insert(name, compiled);
}
Self { engine, rules }
}
pub fn evaluate(&self, rule_name: &str, data: Value) -> Result<Value> {
let compiled = self.rules.get(rule_name).ok_or("Unknown rule")?;
self.engine.evaluate_owned(compiled, data)
}
}
}
Migration Guide
This guide covers migrating between major versions of datalogic-rs.
v3 to v4 Migration
Overview
v4 redesigns the API for ergonomics and simplicity. The core JSONLogic behavior is unchanged, but the Rust API is different.
Key changes:
- Simplified
DataLogicengine API CompiledLogicautomatically wrapped inArc- No more arena allocation (simpler lifetime management)
- New evaluation methods
When to Migrate
Migrate to v4 if:
- Starting a new project
- Want simpler, more ergonomic API
- Don’t need arena-based memory optimization
- Want easier thread safety
Stay on v3 if:
- Already using v3 in production with no issues
- Need maximum performance with arena allocation
- Have complex lifetime requirements
API Changes
Engine Creation
#![allow(unused)]
fn main() {
// v3
use datalogic_rs::DataLogic;
let engine = DataLogic::default();
// v4
use datalogic_rs::DataLogic;
let engine = DataLogic::new();
// v4 with config
use datalogic_rs::{DataLogic, EvaluationConfig};
let engine = DataLogic::with_config(EvaluationConfig::default());
}
Compilation
#![allow(unused)]
fn main() {
// v3
let compiled = engine.compile(&logic)?;
// compiled is not automatically Arc-wrapped
// v4
let compiled = engine.compile(&logic)?;
// compiled is Arc<CompiledLogic>, thread-safe by default
}
Evaluation
#![allow(unused)]
fn main() {
// v3
let result = engine.evaluate(&compiled, &data)?;
// v4 - two options
// Option 1: Takes owned data, returns Value
let result = engine.evaluate_owned(&compiled, data)?;
// Option 2: Takes reference, returns Cow<Value>
let result = engine.evaluate(&compiled, &data)?;
}
Quick Evaluation
#![allow(unused)]
fn main() {
// v3
let result = engine.apply(&logic, &data)?;
// v4
let result = engine.evaluate_json(
r#"{"==": [1, 1]}"#,
r#"{}"#
)?;
}
Custom Operators
#![allow(unused)]
fn main() {
// v3
struct MyOperator;
impl Operator for MyOperator {
fn evaluate(&self, args: &[Value], data: &Value, engine: &DataLogic) -> Result<Value> {
// ...
}
}
// v4
use datalogic_rs::{Operator, ContextStack, Evaluator, Result};
struct MyOperator;
impl Operator for MyOperator {
fn evaluate(
&self,
args: &[Value],
context: &mut ContextStack,
evaluator: &dyn Evaluator,
) -> Result<Value> {
// Arguments are unevaluated - call evaluator.evaluate() as needed
let value = evaluator.evaluate(&args[0], context)?;
// ...
}
}
}
Thread Safety
#![allow(unused)]
fn main() {
// v3 - Manual Arc wrapping
use std::sync::Arc;
let compiled = engine.compile(&logic)?;
let compiled_arc = Arc::new(compiled);
// v4 - Already Arc-wrapped
let compiled = engine.compile(&logic)?; // Already Arc<CompiledLogic>
let compiled_clone = Arc::clone(&compiled);
}
Configuration Changes
#![allow(unused)]
fn main() {
// v3
let engine = DataLogic::default();
// v4
use datalogic_rs::{DataLogic, EvaluationConfig, NanHandling};
let config = EvaluationConfig::default()
.with_nan_handling(NanHandling::IgnoreValue);
let engine = DataLogic::with_config(config);
}
Structured Objects
#![allow(unused)]
fn main() {
// v3
let engine = DataLogic::with_preserve_structure(true);
// v4
let engine = DataLogic::with_preserve_structure();
// v4 with config
let config = EvaluationConfig::default();
let engine = DataLogic::with_config_and_structure(config, true);
}
Error Handling
#![allow(unused)]
fn main() {
// v3
use datalogic_rs::Error;
match engine.evaluate(&compiled, &data) {
Ok(result) => { /* ... */ }
Err(Error::UnknownOperator(op)) => { /* ... */ }
Err(e) => { /* ... */ }
}
// v4 - Same pattern
use datalogic_rs::Error;
match engine.evaluate_owned(&compiled, data) {
Ok(result) => { /* ... */ }
Err(Error::UnknownOperator(op)) => { /* ... */ }
Err(e) => { /* ... */ }
}
}
Migration Checklist
-
Update Cargo.toml:
[dependencies] datalogic-rs = "4.0" -
Update engine creation:
DataLogic::default()→DataLogic::new()
-
Update evaluation calls:
engine.evaluate(&compiled, &data)→engine.evaluate_owned(&compiled, data.clone())- Or use
engine.evaluate(&compiled, &data)for reference-based evaluation
-
Update custom operators:
- Add
context: &mut ContextStackparameter - Replace
engine: &DataLogicwithevaluator: &dyn Evaluator - Call
evaluator.evaluate()on arguments
- Add
-
Remove manual Arc wrapping:
CompiledLogicis now automaticallyArc<CompiledLogic>
-
Test thoroughly:
- Run your test suite
- Verify expected behavior with your specific rules
Performance Considerations
v4 trades some raw performance for a simpler API:
- No arena allocation means more heap allocations
Arcwrapping adds a small overhead for single-threaded use- For most use cases, the difference is negligible
If you need maximum performance:
- Reuse
CompiledLogicinstances - Use
evaluatewith references for large data - Consider staying on v3 for hot paths
Getting Help
If you encounter issues during migration:
- Check the API Reference
- Review the examples
- Open an issue on GitHub
FAQ
Frequently asked questions about datalogic-rs.
General
What is JSONLogic?
JSONLogic is a way to write portable, safe logic rules as JSON. It was created to allow non-developers to create complex rules that can be evaluated consistently across different platforms. The specification is available at jsonlogic.com.
Why use datalogic-rs instead of the reference implementation?
- Performance: datalogic-rs is significantly faster than JavaScript implementations
- Thread Safety: Compiled rules can be safely shared across threads
- Extended Operators: Includes datetime, regex, and additional string/array operators
- Type Safety: Full Rust type system benefits
- WASM Support: Use the same engine in browsers and Node.js
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 v3 or v4?
Use v4 for most projects. It has a simpler, more ergonomic API.
Use v3 only if you need maximum performance with arena allocation and are comfortable with lifetime management.
Both versions are maintained and receive bug fixes.
How do I share compiled rules across threads?
CompiledLogic is wrapped in Arc and is Send + Sync:
#![allow(unused)]
fn main() {
use std::sync::Arc;
use datalogic_rs::DataLogic;
let engine = Arc::new(DataLogic::new());
let compiled = engine.compile(&logic).unwrap();
// Clone the Arc for each thread
let compiled_clone = Arc::clone(&compiled);
std::thread::spawn(move || {
// Use compiled_clone here
});
}
Why do custom operators receive unevaluated arguments?
This design allows operators to implement lazy evaluation (like and and or) and control how arguments are processed. Always call evaluator.evaluate() on arguments that should be evaluated:
#![allow(unused)]
fn main() {
impl Operator for MyOperator {
fn evaluate(&self, args: &[Value], context: &mut ContextStack, evaluator: &dyn Evaluator) -> Result<Value> {
// Evaluate the first argument
let value = evaluator.evaluate(&args[0], context)?;
// ...
}
}
}
What’s the difference between evaluate and evaluate_owned?
evaluate: Takes a reference to data, returnsCow<Value>(avoids cloning when possible)evaluate_owned: Takes ownership of data, returnsValue(simpler API)
Use evaluate for performance-critical code with large data. Use evaluate_owned for simpler code.
JavaScript/WASM Usage
Do I need to call init() in Node.js?
No. The Node.js target doesn’t require initialization:
const { evaluate } = require('@goplasmatic/datalogic');
evaluate('{"==": [1, 1]}', '{}', false); // Works immediately
Why do I need to JSON.stringify my data?
The WASM interface uses string-based communication for maximum compatibility. Always stringify inputs and parse outputs:
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';
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. Set dimensions via CSS or inline styles:
<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 '@xyflow/react/dist/style.css';
import '@goplasmatic/datalogic-ui/styles.css';
import { DataLogicEditor } from '@goplasmatic/datalogic-ui';
export function Editor({ expression }) {
return <DataLogicEditor value={expression} />;
}
When will edit mode be available?
Edit mode is on the roadmap. Check the GitHub issues for updates.
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"] }
Can I use regex?
Yes. Use the match operator:
{ "match": [{ "var": "email" }, "^[a-z]+@example\\.com$"] }
Configuration
How do I handle NaN in arithmetic?
Use the NanHandling configuration:
#![allow(unused)]
fn main() {
use datalogic_rs::{DataLogic, EvaluationConfig, NanHandling};
let config = EvaluationConfig::default()
.with_nan_handling(NanHandling::IgnoreValue);
let engine = DataLogic::with_config(config);
}
Options:
ThrowError(default): Return an errorCoerceToZero: Treat non-numeric as 0IgnoreValue: Skip non-numeric values
How do I change division by zero behavior?
#![allow(unused)]
fn main() {
use datalogic_rs::{EvaluationConfig, DivisionByZero};
let config = EvaluationConfig::default()
.with_division_by_zero(DivisionByZero::ReturnBounds);
}
Options:
ReturnBounds(default): Return Infinity/-InfinityThrowError: Return an errorReturnZero: Return 0
Troubleshooting
“Unknown operator” error
In standard mode, unrecognized keys are treated as errors. Either:
- Fix the operator name (check spelling)
- Register a custom operator
- Enable
preserve_structuremode for templating
Performance issues with large expressions
- Use
CompiledRuleinstead of repeatedevaluatecalls - Consider breaking complex rules into smaller, composable pieces
- Profile with tracing to identify slow sub-expressions
WASM initialization fails
Ensure you’re awaiting init() before calling other functions:
// Wrong
const result = evaluate(...);
// Correct
await init();
const result = evaluate(...);
For more troubleshooting, see the Troubleshooting Guide.
Troubleshooting
Common issues and solutions for datalogic-rs.
Rust Issues
“Unknown operator: xyz”
Cause: Using an unrecognized operator name.
Solutions:
- Check the operator name spelling (operators are case-sensitive)
- Register a custom operator if it’s your own
- Enable
preserve_structuremode if you’re using JSONLogic for templating
#![allow(unused)]
fn main() {
// Option 1: Fix spelling
let logic = json!({ "and": [...] }); // not "AND"
// Option 2: Custom operator
engine.add_operator("xyz".to_string(), Box::new(XyzOperator));
// Option 3: Templating mode
let engine = DataLogic::with_preserve_structure();
}
“Variable not found”
Cause: Accessing a path that doesn’t exist in the data.
Solutions:
- Check the variable path spelling
- Use a default value
- Use
missingto check first
#![allow(unused)]
fn main() {
// Default value
let logic = json!({ "var": ["user.name", "Anonymous"] });
// Check first
let logic = json!({
"if": [
{ "missing": ["user.name"] },
"No name",
{ "var": "user.name" }
]
});
}
“NaN” or unexpected arithmetic results
Cause: Non-numeric values in arithmetic operations.
Solution: Configure NaN handling:
#![allow(unused)]
fn main() {
use datalogic_rs::{EvaluationConfig, NanHandling};
let config = EvaluationConfig::default()
.with_nan_handling(NanHandling::IgnoreValue); // or CoerceToZero
let engine = DataLogic::with_config(config);
}
Thread safety errors
Cause: Custom operators that aren’t Send + Sync.
Solution: Ensure custom operators are thread-safe:
#![allow(unused)]
fn main() {
// This won't compile if operator uses RefCell, Rc, etc.
engine.add_operator("my_op".to_string(), Box::new(MyOperator));
// Use Arc, Mutex, or make fields immutable
struct MyOperator {
config: Arc<Config>, // Thread-safe
}
}
Slow compilation
Cause: Very large or deeply nested expressions.
Solutions:
- Compile once, evaluate many times
- Break expressions into smaller pieces
- Consider using
preserve_structurefor simpler parsing
#![allow(unused)]
fn main() {
// Compile once
let compiled = engine.compile(&logic)?;
// Evaluate many times
for data in dataset {
engine.evaluate_owned(&compiled, data)?;
}
}
JavaScript/WASM Issues
“RuntimeError: memory access out of bounds”
Cause: WASM module not initialized.
Solution: Call init() before using any functions:
import init, { evaluate } from '@goplasmatic/datalogic';
await init(); // Must await before using evaluate
evaluate(logic, data, false);
“TypeError: Cannot read properties of undefined”
Cause: Using the wrong import style for your environment.
Solutions:
// Browser/Bundler - need default import for init
import init, { evaluate } from '@goplasmatic/datalogic';
// Node.js - no init needed
const { evaluate } = require('@goplasmatic/datalogic');
“Failed to fetch” in browser
Cause: WASM file not accessible from the browser.
Solutions:
- Check your bundler configuration
- Ensure WASM files are being served correctly
- Check CORS headers if loading from CDN
For Vite, it should work automatically. For Webpack:
// webpack.config.js
module.exports = {
experiments: {
asyncWebAssembly: true,
},
};
Results are strings, not values
Cause: WASM returns JSON strings, not native values.
Solution: Parse the result:
const resultString = evaluate(logic, data, false);
const result = JSON.parse(resultString); // Parse to native value
Performance issues
Cause: Recompiling rules repeatedly.
Solution: Use CompiledRule:
// Slow - compiles each time
for (const item of items) {
evaluate(logic, JSON.stringify(item), false);
}
// Fast - compile once
const rule = new CompiledRule(logic, false);
for (const item of items) {
rule.evaluate(JSON.stringify(item));
}
React UI Issues
“ResizeObserver loop completed with undelivered notifications”
Cause: Container size changes rapidly.
Solution: This warning is usually harmless, but you can debounce size changes:
function StableEditor({ expression }) {
const containerRef = useRef<HTMLDivElement>(null);
return (
<div ref={containerRef} style={{ height: '500px' }}>
<DataLogicEditor value={expression} />
</div>
);
}
Editor shows blank/empty
Causes:
- Container has no dimensions
- CSS not imported
- Expression is null
Solutions:
// 1. Ensure container has dimensions
<div style={{ width: '100%', height: '500px' }}>
<DataLogicEditor value={expression} />
</div>
// 2. Import CSS in correct order
import '@xyflow/react/dist/style.css';
import '@goplasmatic/datalogic-ui/styles.css';
// 3. Check expression
<DataLogicEditor value={expression ?? { "==": [1, 1] }} />
Debug mode not showing results
Cause: data prop not provided.
Solution: Debug mode requires data:
<DataLogicEditor
value={expression}
data={{ x: 1, y: 2 }} // Required for debug mode
mode="debug"
/>
SSR/Hydration errors in Next.js
Cause: WASM doesn’t run on server.
Solution: Use client component:
'use client';
import dynamic from 'next/dynamic';
const DataLogicEditor = dynamic(
() => import('@goplasmatic/datalogic-ui').then(mod => mod.DataLogicEditor),
{ ssr: false }
);
Build Issues
WASM build fails
Cause: Missing wasm-pack or target.
Solution:
# Install wasm-pack
cargo install wasm-pack
# Add target
rustup target add wasm32-unknown-unknown
# Build
cd wasm && ./build.sh
TypeScript errors with imports
Cause: Missing type declarations or wrong import.
Solution: Check your tsconfig.json:
{
"compilerOptions": {
"moduleResolution": "bundler", // or "node16"
"allowSyntheticDefaultImports": true
}
}
Bundler can’t find WASM file
Cause: WASM file not copied to output.
Solution: Depends on bundler:
// Vite - usually automatic
// Webpack - enable async WASM
experiments: { asyncWebAssembly: true }
// Rollup - use @rollup/plugin-wasm
Getting Help
If you can’t resolve an issue:
- Check existing issues
- Create a minimal reproduction
- Open a new issue with:
- datalogic-rs version
- Environment (Rust/Node/Browser)
- Minimal code to reproduce
- Expected vs actual behavior