Rule Cascade
LearnOperators

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

null and type operators
{ op: exists, args: [{ var: data.coupon }] }
{ op: coalesce, args: [{ var: data.shipping }, 0] }
{ op: typeOf, args: [{ var: data.quantity }] }
OperatorArgumentsResult
existsanytrue unless the value is null
emptyanytrue for null, "", [] and {}
coalesceany numberThe first argument that is not null, else null. Later arguments are not evaluated
typeOfany"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.

null.ruleset.yaml
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: [] }
request.json
{
  "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./quantity
  • NUL-002errorblockingWrite a message for the gift./giftMessage
  • NUL-003errorblockingA coupon code has 8 characters./coupon
Try it YourselfOpens this ruleset and request in the playground. Nothing to install.

Common mistakes

  • exists is true for "". Use empty when an empty string counts as missing.
  • A missing field is null, and most operators fail on null. Guard with when and exists, or give a default with coalesce.
  • typeOf says "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
null.ruleset.yaml
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: [] }
request.json
{
  "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
Course overview

On this page