Null and type operators
exists, empty, coalesce and typeOf handle missing values and check types.
A field that is missing reads as null. exists tells you whether a value is there. empty is
also true for "", [] and {}. coalesce picks the first value that is not null. typeOf
names the type of a value, because the engine never converts one type into another.
Syntax
{ op: exists, args: [{ var: data.coupon }] }
{ op: coalesce, args: [{ var: data.shipping }, 0] }
{ op: typeOf, args: [{ var: data.quantity }] }| Operator | Arguments | Result |
|---|---|---|
exists | any | true unless the value is null |
empty | any | true for null, "", [] and {} |
coalesce | any number | The first argument that is not null, else null. Later arguments are not evaluated |
typeOf | any | "null", "boolean", "number", "string", "list" or "object" |
Example
The request sends the quantity as the text "3", wraps a gift with an empty message, and has a
coupon of six characters.
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.null, version: 1.0.0, title: Null and type operators }
scope:
- { level: organization, id: learn }
entities:
Order:
schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
rules:
- id: order.quantity.number
kind: validation
target: { entity: Order, field: /quantity }
operations: [create]
assert: { op: eq, args: [{ op: typeOf, args: [{ var: data.quantity }] }, "number"] }
severity: error
finding:
code: NUL-001
message: quantity.number
args: { type: { op: typeOf, args: [{ var: data.quantity }] } }
- id: order.gift-message.required
kind: validation
target: { entity: Order, field: /giftMessage }
operations: [create]
when: { op: eq, args: [{ var: data.giftWrap }, true] }
assert: { op: not, args: [{ op: empty, args: [{ var: data.giftMessage }] }] }
severity: error
finding: { code: NUL-002, message: giftMessage.required }
- id: order.coupon.length
kind: validation
target: { entity: Order, field: /coupon }
operations: [create]
when: { op: exists, args: [{ var: data.coupon }] }
assert: { op: eq, args: [{ op: len, args: [{ var: data.coupon }] }, 8] }
severity: error
finding: { code: NUL-003, message: coupon.length }
- id: order.shipping.max
kind: validation
target: { entity: Order, field: /shipping }
operations: [create]
assert: { op: lte, args: [{ op: coalesce, args: [{ var: data.shipping }, 0] }, 50] }
severity: error
finding: { code: NUL-004, message: shipping.max }
messages:
en:
quantity.number: "The quantity must be a number, not a {type}."
giftMessage.required: "Write a message for the gift."
coupon.length: "A coupon code has 8 characters."
shipping.max: "Shipping costs at most 50."
tests:
- name: a quantity sent as text, an empty gift message and a short coupon
entity: Order
operation: create
given:
data: { quantity: "3", giftWrap: true, giftMessage: "", coupon: "SAVE10" }
expect:
decision: deny
findings:
- { rule: order.quantity.number, message: "The quantity must be a number, not a string." }
- { rule: order.gift-message.required }
- { rule: order.coupon.length }
- name: missing optional fields are fine
entity: Order
operation: create
given:
data: { quantity: 3 }
expect: { decision: allow, findings: [] }{
"entity": "Order",
"operation": "create",
"data": {
"quantity": "3",
"giftWrap": true,
"giftMessage": "",
"coupon": "SAVE10"
}
}Result, from the engine
Decisiondeny3 findings, server channel
NUL-001errorblockingThe quantity must be a number, not a string./quantityNUL-002errorblockingWrite a message for the gift./giftMessageNUL-003errorblockingA coupon code has 8 characters./coupon
Common mistakes
existsis true for"". Useemptywhen an empty string counts as missing.- A missing field is
null, and most operators fail onnull. Guard withwhenandexists, or give a default withcoalesce. typeOfsays"number"for integers and decimals alike.
Exercise
A customer must give an e-mail address or a phone number. An empty string counts as missing. Write
one rule on Customer.
Hint
Target both fields with fields: [/email, /phone]. The rule passes when one of the two is not empty.
Show answer
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.null, version: 1.0.0, title: Null and type operators }
scope:
- { level: organization, id: learn }
entities:
Customer:
schema: { $ref: "./learn.openapi.yaml#/components/schemas/Customer" }
rules:
- id: customer.contact.required
kind: validation
target: { entity: Customer, fields: [/email, /phone] }
operations: [create]
assert:
op: or
args:
- { op: not, args: [{ op: empty, args: [{ var: data.email }] }] }
- { op: not, args: [{ op: empty, args: [{ var: data.phone }] }] }
severity: error
finding: { code: NUL-001, message: contact.required }
messages:
en:
contact.required: "Give an e-mail address or a phone number."
tests:
- name: no contact at all is denied
entity: Customer
operation: create
given:
data: { name: "Ana", email: "" }
expect:
decision: deny
findings:
- { rule: customer.contact.required, fields: [/email, /phone] }
- name: a phone number alone is enough
entity: Customer
operation: create
given:
data: { name: "Ana", phone: "+34612345678" }
expect: { decision: allow, findings: [] }{
"entity": "Customer",
"operation": "create",
"data": {
"name": "Ana",
"email": ""
}
}Result, from the engine
Decisiondeny1 finding, server channel
NUL-001errorblockingGive an e-mail address or a phone number./email, /phone