Values, null and no coercion
The string "1" is not the number 1, a missing path is null, equality is deep, and list indexes are path segments.
Values are JSON values, and the engine never converts one type into another. The number 1 is not
the string "1", and true is not 1. A path that does not exist evaluates to null. In a path,
a segment of digits is a list index: data.items.0.sku is the SKU of the first line.
Syntax
{ op: eq, args: [{ var: data.priority }, 1] } # true only for the number 1
{ op: eq, args: [{ var: data.priority }, "1"] } # true only for the string "1"
{ var: data.items.0.sku } # first element; null if absent| Rule | Meaning |
|---|---|
| No coercion | 1 is not "1"; true is not 1 |
Missing is null | A path that does not resolve is null, never an error |
| Deep equality | eq compares lists element by element and objects key by key; 1 equals 1.0 |
| Indexes | A digits-only segment indexes a list; past the end is null |
| Type errors | lt on a string, a wrong argument count, an unknown operator: an evaluation error |
Example
An express order needs priority 1. The request sends the string "1", so it is denied.
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.no-coercion, version: 1.0.0, title: "No coercion" }
scope:
- { level: organization, id: learn }
entities:
Order:
schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
rules:
- id: order.express.priority
kind: validation
target: { entity: Order, field: /priority }
operations: [create]
when: { op: eq, args: [{ var: data.express }, true] }
assert: { op: eq, args: [{ var: data.priority }, 1] }
severity: error
finding: { code: LRN-VAL-001, message: order.expressPriority }
- id: order.first-item.sku
kind: validation
target: { entity: Order, field: /items }
operations: [create]
when: { op: exists, args: [{ var: data.items }] }
assert: { op: exists, args: [{ var: data.items.0.sku }] }
severity: error
finding: { code: LRN-VAL-002, message: order.firstItemSku }
messages:
en:
order.expressPriority: "Express orders have priority 1."
order.firstItemSku: "The first line needs a SKU."
tests:
- name: the string "1" is not the number 1
entity: Order
operation: create
given:
data: { express: true, priority: "1" }
expect:
decision: deny
findings:
- { rule: order.express.priority, fields: [/priority] }
- name: the number 1 passes
entity: Order
operation: create
given:
data: { express: true, priority: 1 }
expect: { decision: allow, findings: [] }
- name: a missing SKU is null, so exists is false
entity: Order
operation: create
given:
data: { items: [{ qty: 1 }] }
expect:
decision: deny
findings:
- { rule: order.first-item.sku, fields: [/items] }{
"entity": "Order",
"operation": "create",
"data": {
"express": true,
"priority": "1"
}
}Result, from the engine
Decisiondeny1 finding, server channel
LRN-VAL-001errorblockingExpress orders have priority 1./priority
Common mistakes
- Sending numbers as strings from a form. Convert form input before you evaluate, or accept both types on purpose.
- Unquoted YAML scalars. YAML 1.1 parsers read
no,on,yes,012and2026-10-03as other types than YAML 1.2 parsers do. Quote them:"no","2026-10-03". The checker reports an unquoted one asYAML_NOT_PORTABLE. Do not use anchors, aliases or merge keys either. - Treating
nullas an error.existsandemptytest for it;coalescegives a default.
Exercise
Make the priority rule accept both the number 1 and the string "1", and nothing else. Change the
first golden test so it expects allow.
Hint
Wrap two eq calls in an or: one compares with the number 1, the other with the string "1".
Show answer
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.no-coercion, version: 1.0.0, title: "No coercion" }
scope:
- { level: organization, id: learn }
entities:
Order:
schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
rules:
- id: order.express.priority
kind: validation
target: { entity: Order, field: /priority }
operations: [create]
when: { op: eq, args: [{ var: data.express }, true] }
assert: { op: or, args: [{ op: eq, args: [{ var: data.priority }, 1] }, { op: eq, args: [{ var: data.priority }, "1"] }] }
severity: error
finding: { code: LRN-VAL-001, message: order.expressPriority }
- id: order.first-item.sku
kind: validation
target: { entity: Order, field: /items }
operations: [create]
when: { op: exists, args: [{ var: data.items }] }
assert: { op: exists, args: [{ var: data.items.0.sku }] }
severity: error
finding: { code: LRN-VAL-002, message: order.firstItemSku }
messages:
en:
order.expressPriority: "Express orders have priority 1."
order.firstItemSku: "The first line needs a SKU."
tests:
- name: the string "1" is accepted now
entity: Order
operation: create
given:
data: { express: true, priority: "1" }
expect: { decision: allow, findings: [] }
- name: priority 2 is still denied
entity: Order
operation: create
given:
data: { express: true, priority: 2 }
expect:
decision: deny
findings:
- { rule: order.express.priority, fields: [/priority] }
- name: the number 1 passes
entity: Order
operation: create
given:
data: { express: true, priority: 1 }
expect: { decision: allow, findings: [] }
- name: a missing SKU is null, so exists is false
entity: Order
operation: create
given:
data: { items: [{ qty: 1 }] }
expect:
decision: deny
findings:
- { rule: order.first-item.sku, fields: [/items] }{
"entity": "Order",
"operation": "create",
"data": {
"express": true,
"priority": "1"
}
}Result, from the engine
Decisionallow0 findings, server channel