Why JSON-Serializable Rules Are Worth the Constraint

JSON-serializable rules trade arbitrary code for portability, auditability, and safer boundaries—if activation is treated like a release.

Geometric rule structure surrounded by JSON objects and code

Making a rule JSON-serializable sounds like an implementation detail. In practice, it changes the architecture.

A JavaScript callback can close over local variables, call a service, read the clock, or mutate state. JSON cannot do any of that. It can only describe data. That limitation is the point.

What the constraint buys you

Consider a pricing eligibility rule:

{
"and": [
{ "eq": ["customer.tier", "gold"] },
{ "gte": ["order.total", 100] },
{ "lt": ["inventory.remaining", 10] }
]
}

Because the rule contains no executable code, it can cross process boundaries. The same document can be stored, reviewed, cached, compared, and sent over an API.

It also becomes possible to answer operational questions without reconstructing a deployment:

  • Which rule version produced this decision?
  • What changed between version 12 and 13?
  • Who approved the active version?
  • Can we replay yesterday’s input against both versions?

Those capabilities do not come from JSON alone. JSON simply makes them practical.

What the constraint takes away

Serializable rules are less expressive than a programming language. That is healthy until teams try to rebuild a programming language inside the rule format.

Custom operators are the escape hatch. Use them for stable domain concepts, not one campaign’s special case. An operator such as isBusinessDay may be reusable. An operator named after a temporary promotion is business data disguised as code.

Rules should also be deterministic. Pass volatile values such as the current time, exchange rate, or feature assignment into the evaluation context. Hidden reads make replay and auditing unreliable.

const context = {
customer,
order,
inventory,
evaluatedAt: new Date().toISOString(),
ruleVersion: 13,
};

Activation still needs engineering discipline

Moving a rule out of the application removes the application deployment. It does not remove risk.

A production rule system needs:

  • schema and operator validation;
  • maximum depth and complexity limits;
  • draft, review, and activation states;
  • immutable versions with actor and timestamp;
  • canary or shadow evaluation for sensitive changes;
  • an immediate rollback path;
  • metrics keyed by rule ID and version.

For authorization, fraud, lending, or pricing, the server remains authoritative. Client-side evaluation may improve feedback, but it must not become the security boundary.

Typed authoring helps

rule-engine-js v1.0.7 added typed path autocomplete:

type Context = {
user: { age: number };
order: { total: number };
};
const rules = createRuleHelpers<Context>();
rules.gte("user.age", 18);
rules.gt("order.total", 0);

The generic helpers catch many path and value mistakes while a rule is authored in TypeScript. Remotely supplied JSON still needs runtime validation.

Serializable rules are worthwhile when portability and controlled change matter more than unrestricted expression. The constraint is not a workaround. It is the boundary that makes the system understandable.

Sources: repository, v1.0.7 changelog.

Read more

All posts
  1. Glowing geometric rules transforming into structured JSON objects
  2. Business rules moving from a dense application core into separate modules
  3. Tangled conditional paths becoming clean independently tested rules
  4. Herdr agent workspace managed as part of the dotfiles setup