7.6 Transforms with JSONata

A transform step reshapes the in-flight JSON body using JSONata, a compact expression language for querying and transforming JSON. It's how you turn a service's response into the exact shape the next step — or the caller — needs.

Two forms

  • Template object — a JSON object whose string leaves are each evaluated as a JSONata expression (recursively; non-string scalars pass through untouched):

    { "transform": {
        "orderId": "$order.id",
        "total":   "$sum($order.lines.price)",
        "charged": "$_ok"
    } }
    
  • Bare string template — a single expression evaluated against the whole body:

    { "transform": "$.items[status = 'open']" }
    

The input and the variables

  • The transform's input (the JSONata $) is the current in-flight body.
  • Captured variables are available by name: $order, $customer, … (7.5).
  • Runtime values: $_status, $_ok, $_headers, $_rawBody (the raw request bytes, for signature checks).
  • Caller and URL: $_user / $principal carries the authenticated caller's email, roles, and configured extra JWT claims; $_url carries the triggering request's path segments and query. $_user is unbound for an anonymous caller, so test it with $exists before reading it.

So a transform can combine the current body, earlier captures, and the response status into one new object.

Host functions

Beyond stock JSONata, the host registers a few functions that need primitives JSONata doesn't expose. All fail closed — bad input yields "" or false rather than an error — so a gate built on them denies by default.

  • $hmac(algo, key, message) → lowercase hex MAC (algo is sha256 or sha512).
  • $hmacVerify(algo, key, message, signatureHex) → bool, constant-time. The webhook-gating pattern: $hmacVerify('sha256', $secret, $_rawBody, $sig) ? $ : $error('bad signature'), with the signing key bound as a secret-backed variable.
  • $hashPassword(password) → an argon2id PHC string — the same scheme the auth service mints on login. A provisioning pipeline can hash a plaintext password before PUTting a user record, so seeded users log in unchanged without a separate hashing step.
  • $verifyPassword(password, hash) → bool. Accepts argon2id PHC strings and legacy bcrypt hashes.

What JSONata buys you

JSONata handles the reshaping work you'd otherwise write code for: projecting and renaming fields, filtering arrays (items[price > 100]), aggregating ($sum, $count, $average), mapping, conditionals (cond ? a : b), string functions, and building new object/array structures. A transform step keeps that logic declarative and inside the pipeline rather than pushing you to a custom service.

Every expression leaf is parse-checked when the pipeline is stored — including untaken branches and nested pipelines. Syntax errors are therefore a 422 validation_failed on the spec PUT. Errors that need runtime values, such as an unbound variable or misspelled function, still surface when evaluated.

Transforms are materialization points

One behavior to keep in mind for later: a transform materializes the body (it needs the JSON in hand to evaluate). That makes it a natural segment boundary for retries (7.8) — which is usually what you want, since a transform is a clean checkpoint between a fetch and an effect. Place a transform before an unsafe call to give that call its own retryable segment.

When a transform isn't enough

JSONata covers a lot, but if you need real control flow, external libraries, or non-trivial algorithms, that's the signal to write a custom service (Part 8) and call it from the pipeline instead. Transforms are for shaping data, not for arbitrary computation.

Next: making pipelines resilient — retries, effect classes, and idempotency.


Previous: 7.5 Conditions, interpolation, variables · Manual home · Next: 7.7 Retries, effect classes, idempotency →