← Docs

Templates

A template rewrites the JSON body of a request before Middleman forwards it. You write the JSON you want to send and fill in values from the incoming body (the input).

Cheat sheet

WriteTo
$.email $read a field of the input, or the whole input
$.a.b $.a["x-id"] $.a[0] $.a[-1] $.a[1:3] $["x-id"]drill into objects and arrays
+ - * / % == != < <= > >= && || !compute, compare, combine
"user_{$.id}"put a value in text
a ?? ba, or b when a is missing or null
exists($.a.b)tell a field the sender left out from one sent as null
len(x)count elements, characters or fields
s[0:3] lower(s) split(s, ",") join(xs, ",") replace(s, a, b)work with text
toUnix(d) fromUnix(n) base64(s) sha256(s)work with dates and encodings
number(x) string(x) toJSON(x) fromJSON(s)convert between text, numbers and JSON
if (c) a : bpick a value
if (c) aleave a field or element out when c is false
for (x in xs) ...map or filter an array, or loop over an object
[...xs] {...obj}flatten arrays or merge objects
let x = ...name a value to reuse
@headers @query @method @pathread the request's headers, query, method and path
[expr]: valuecompute a field name
// notecomment to end of line

Fields

$.email reads that field of the input: $ is the whole input and .email picks a field of it. A name with no $. in front is a variable, never an input field. A field whose name isn't a plain word is read as $["first-name"]. Everything else is plain JSON (trailing commas allowed). Fields keep the order you write them in, and numbers pass through exactly as sent.

Input

{
  "user": {"first-name": "Ada", "email": "ada@example.com"},
  "orders": [{"id": 7}, {"id": 8}, {"id": 9}]
}

Template

{
  "to": $.user.email,
  "name": $.user["first-name"],
  "last": $.orders[-1].id,
  "rest": $.orders[1:]
}

Output

{
  "to": "ada@example.com",
  "name": "Ada",
  "last": 9,
  "rest": [{"id": 8}, {"id": 9}]
}

Reading a field that isn't there is an error, so a typo never sends null. To handle an optional field, give it a default with ??, or compare it to null, which reads a missing field as null anywhere:

Input

{"name": "Ada"}

Template

{
  "user": $.id ?? "guest",
  "has_id": $.id != null
}

Output

{"user": "guest", "has_id": false}

Operators

From tightest to loosest: ! - (unary), * / %, + -, ??, < > <= >=, == !=, &&, ||. Use parentheses to group.

Input

{"id": 42, "qty": 3, "price": 250, "status": "vip", "items": [1, 2]}

Template

{
  "ref": "user_{$.id}",
  "total": $.qty * $.price * 1.2,
  "vip": $.status == "vip",
  "count": len($.items)
}

Output

{
  "ref": "user_42",
  "total": 900,
  "vip": true,
  "count": 2
}

Text

Write {expr} inside a string to put a value in it: "user_{$.id}", "{$.first} {$.last}". Any expression works, including strings (which can interpolate in turn) and objects with braces of their own; the expression ends at the } that closes it. Only strings and numbers can be interpolated, and a number keeps the text it was sent with (1.50 stays 1.50). Interpolating anything else, such as null or a missing value, is an error, so you never send "user_null".

To write a literal { in a string, escape it as \{; a lone } needs no escape. A { that doesn't start a valid expression, as in "{}" or "{name}" with no variable name, is an error when you save, and the message suggests \{. Comments are not allowed inside the braces. A field name interpolates too: "user_{$.id}": true.

Input

{"id": 7, "first": "Ada", "last": "Lovelace", "price": 1.50}

Template

{
  "ref": "user_{$.id}",
  "name": "{$.first} {$.last}",
  "label": "{if ($.price > 1) "pricey" : "cheap"}: {$.price}",
  "literal": "a \{brace} and a } too"
}

Output

{
  "ref": "user_7",
  "name": "Ada Lovelace",
  "label": "pricey: 1.50",
  "literal": "a {brace} and a } too"
}

Text functions

These work on strings and fail on anything else. Positions count characters, not bytes, so "héllo"[1] is "é".

Input

{
  "name": "  Ada Lovelace ",
  "tags": "a,b,c",
  "ids": [1, 2.50, "x"],
  "email": "ADA@Example.com"
}

Template

{
  "initial": trim($.name)[0],
  "first": split(trim($.name), " ")[0],
  "tags": split($.tags, ","),
  "ids": join($.ids, "-"),
  "email": lower($.email),
  "dashed": replace($.tags, ",", "-"),
  "is_example": endsWith(lower($.email), "example.com")
}

Output

{
  "initial": "A",
  "first": "Ada",
  "tags": ["a", "b", "c"],
  "ids": "1-2.50-x",
  "email": "ada@example.com",
  "dashed": "a-b-c",
  "is_example": true
}

Dates and encodings

Dates are strings in RFC 3339 form (2024-01-31T12:00:00Z, with an offset or fractions of a second if you like); Unix times are numbers.

Input

{"at": "2024-01-31T12:00:00Z", "ms": 1706702400123, "token": "ada:secret"}

Template

{
  "seconds": toUnix($.at),
  "millis": toUnixMs($.at),
  "date": fromUnixMs($.ms),
  "encoded": base64($.token),
  "decoded": unbase64(base64($.token)),
  "digest": sha256($.token)
}

Output

{
  "seconds": 1706702400,
  "millis": 1706702400000,
  "date": "2024-01-31T12:00:00.123Z",
  "encoded": "YWRhOnNlY3JldA==",
  "decoded": "ada:secret",
  "digest": "7a57c89b3477a13317bc0d38118202f9d36e9804547ccf18e753754e041e0aee"
}

Converting values

Senders often send numbers as text, and some targets want JSON inside a string. Four functions convert, and each takes exactly one argument:

Input

{
  "amount": "10",
  "price": 1.50,
  "meta": {"id": 7},
  "payload": "{\"tags\": [\"a\", \"b\"]}"
}

Template

{
  "big": number($.amount) > 5,
  "total": number($.amount) * 2,
  "price": string($.price),
  "meta_text": toJSON($.meta),
  "tags": fromJSON($.payload).tags
}

Output

{
  "big": true,
  "total": 20,
  "price": "1.50",
  "meta_text": "{\"id\":7}",
  "tags": ["a", "b"]
}

Conditionals

if (c) a : b gives a when c is true, b otherwise. Conditions must be true, false or null (which counts as false); nothing else counts as “truthy”.

Input

{"score": 72, "is_premium": false}

Template

{
  "tier": if ($.is_premium) "paid" : "free",
  "grade": if ($.score >= 90) "A"
    : if ($.score >= 70) "B" : "C"
}

Output

{"tier": "free", "grade": "B"}

Optional fields

Leave out : b and a false condition produces nothing: the field is omitted, the array element dropped, and a template left out entirely produces an empty body (see below). Written in place of a field, an if adds a whole group of fields.

Input

{"discount": null, "gift": true, "plan": "pro"}

Template

{
  "code": if ($.discount != null) $.discount,
  "extras": ["base", if ($.gift) "wrap"],
  if ($.plan != "free") {
    "plan": $.plan,
    "paid": true
  }
}

Output

{
  "extras": ["base", "wrap"],
  "plan": "pro",
  "paid": true
}

A template that is absent as a whole does not filter the request. It is still forwarded, with an empty body and the original Content-Type (the request template you wrote is honored as is), and a target that expects JSON may reject an empty body.

To tell a field the sender left out from one sent as null, use exists(path). It is true when every step of the path is there (a null counts as there) and false otherwise. It only takes a path, like $.a.b, $.xs[0] or a variable, and a step on the wrong type (a field of a string) is still an error. A variable holding nothing, from an if without : b, doesn’t exist. A null in the middle of the path is still an error (exists($.a.b) fails when a is null), so use $.a != null to guard it.

Input

{"note": null, "title": "Hi"}

Template

{
  "title": if (exists($.title)) $.title,
  "note": if (exists($.note)) $.note,
  "tags": if (exists($.tags)) $.tags
}

Output

{"title": "Hi", "note": null}

Loops

for (x in xs) body builds an array from each element of xs. for (i, x in xs) also gives the index, and for (k, v in obj) loops over an object's fields. A body that is an if without : b filters.

Input

{"orders": [{"id": 1, "paid": true}, {"id": 2, "paid": false}]}

Template

{
  "lines": for (i, o in $.orders) {
    "n": i + 1, "ref": "order_{o.id}"
  },
  "paid": for (o in $.orders) if (o.paid) o.id
}

Output

{
  "lines": [
    {"n": 1, "ref": "order_1"},
    {"n": 2, "ref": "order_2"}
  ],
  "paid": [1]
}

Inside an array, a loop adds its results as elements. In place of a field, it merges each result's fields into the object, e.g. to turn name/value pairs into an object:

Input

{
  "headers": [
    {"name": "X-Env", "value": "prod"},
    {"name": "X-Team", "value": "core"}
  ],
  "extra": ["c"]
}

Template

{
  for (h in $.headers) { [h.name]: h.value },
  "tags": ["a", "b", for (t in $.extra) t]
}

Output

{
  "X-Env": "prod",
  "X-Team": "core",
  "tags": ["a", "b", "c"]
}

Spread

...xs in an array adds each element of the array xs, so [...$.a, ...$.b] joins two arrays and a loop whose body is a spread flattens. In an object, ...obj merges the fields of obj; a field that is already there is replaced, in its place. A spread of nothing (an if without : b that is false) adds nothing, and spreading the wrong kind (an array in an object, an object in an array, null or a scalar) is an error. A spread is only allowed where it adds something: directly in an array or object, as the body of a for, or as a branch of an if that is itself there. Anywhere else (an if in a field value, a let, a function argument, an operand) it is rejected when the template is saved.

Input

{
  "orders": [{"items": ["a", "b"]}, {"items": ["c"]}],
  "user": {"name": "Ada", "role": "dev"}
}

Template

{
  ...$.user,
  "role": "admin",
  "items": [for (o in $.orders) ...o.items, "d"]
}

Output

{
  "name": "Ada",
  "role": "admin",
  "items": ["a", "b", "c", "d"]
}

Variables

let name = value names a value for reuse, at the top of the template, inside an object before the fields that use it, or at the start of a parenthesized value, (let y = x * 2; y + 1). Inside the braces or parentheses it is visible only there. A let ends at the end of its line or at ;, never at a comma. To continue one, start the next line with an operator like + or -, or break inside brackets. A line starting with [ starts the next value, and so does one starting with (, except inside an object, where ( continues the let (no field starts with one). A template whose value starts with a negation right after a let line needs parentheses, (-x), or a ; after the let. A variable and an input field can share a name: a bare name is always a variable and $.name always an input field. A bare name that isn't declared is an error when you save.

Input

{"items": [{"qty": 2, "price": 5}, {"qty": 1, "price": 7}], "currency": "EUR"}

Template

let cur = " {$.currency}"
for (it in $.items) {
  let total = it.qty * it.price
  "total": "{total}{cur}",
  "big": total >= 10
}

Output

[
  {"total": "10 EUR", "big": true},
  {"total": "7 EUR", "big": false}
]

Parentheses let a for name a value even when its body isn't an object, and the line rules above apply inside them as they do at the top level:

Input

{"xs": [1, 2, 3]}

Template

for (x in $.xs) (
  let y = x * 2
  y + 1
)

Output

[3, 5, 7]

Field names

"name": value is a fixed name, used at most once per object. A string with { } in it computes its name. [expr]: value computes one, as does an input field like $.ref: value or $.a.b: value, or a variable like v: value. Names must be strings or numbers.

Input

{"event": "order.paid", "payload": {"id": 5}, "id": 5}

Template

{
  $.event: $.payload,
  "user_{$.id}": true
}

Output

{
  "order.paid": {"id": 5},
  "user_5": true
}

Request details

The body isn't all a request says: webhooks often put what the target needs in a header or the URL. Four variables starting with @ hold those. They are predeclared and read-only, so you can't declare a variable with such a name, and any other @name is an error when you save (with a "did you mean" when it is close to one of these).

Input

POST /orders/5?page=2&page=3
Accept: text/plain
Accept: application/json
X-Github-Event: push

{"id": 5}

Template

{
  "event": @headers["x-github-event"],
  "accept": @headers.accept,
  "page": @query.page ?? "1",
  "route": "{@method} {@path}",
  "id": $.id
}

Output

{
  "event": "push",
  "accept": "text/plain, application/json",
  "page": "2",
  "route": "POST /orders/5",
  "id": 5
}

Errors

Errors in a template point at the line:col where they happened (an input that is not valid JSON is reported without a position):

Input

{"user": {"name": "Ada"}}

Template

{
  "name": $.user.name,
  "email": $.user.email
}

Error

3:18: field "email" not found

An error inside a for also names the iteration it happened in, outermost loop first:

Input

{"orders": [{"price": 5}, {}]}

Template

for (o in $.orders) o.price

Error

1:22: field "price" not found (in $.orders[1])

Some errors end with a likely fix: brackets for a name with a - in it ($.user.first-name reads as $.user.first - name), or != null or ?? for a condition that is set but isn't true or false.

A template also stops with an error if it does too much work (say, nested loops over large arrays), produces more than 10 MiB, or meets an input or template nested more than 1000 levels deep.

← Docs