Skip to content

Response Templating

Mock responses don’t have to be fully static JSON. Use {{ }} double-brace syntax inside any string value in response (or a sequence step’s response) to echo back data from the incoming request — no custom code required.

This dramatically cuts down on how many mock files you need. Instead of one file per id, username, or header value, one templated mock covers all of them.

Template Source
{{path.id}} A named path parameter :id or {id}
{{query.page}} URL query parameter ?page=2
{{header.x-request-id}} Request header value (case-insensitive)
{{body.username}} Top-level JSON (or form) body field
{{body.user.email}} Nested JSON body field, using dot notation
{{faker.uuid}} Randomly generated data — no request input needed
{
"method": "POST",
"path": "/users",
"status": 201,
"response": {
"id": 99,
"username": "{{body.username}}",
"email": "{{body.email}}",
"created_by": "{{header.x-actor}}",
"self_url": "/users/99"
}
}
Terminal window
curl -X POST http://localhost:8080/users \
-H "X-Actor: admin" \
-H "Content-Type: application/json" \
-d '{"username":"alice","email":"[email protected]"}'
{
"id": 99,
"username": "alice",
"email": "[email protected]",
"created_by": "admin",
"self_url": "/users/99"
}

Templating is what makes path parameters useful for more than routing — the captured value can flow straight into the response:

{
"method": "GET",
"path": "/users/:id",
"status": 200,
"response": { "id": "{{path.id}}", "name": "Mock User" }
}
Terminal window
curl http://localhost:8080/users/42
# { "id": "42", "name": "Mock User" }

{{body.…}} templates can index into arrays as well as objects — a zero-based integer segment reads an array element, in either dot or bracket syntax:

{
"method": "POST",
"path": "/orders",
"status": 201,
"response": {
"first_sku": "{{body.items.0.sku}}",
"first_sku_bracket": "{{body.items[0].sku}}",
"first_qty": "{{number:body.items[0].qty}}"
}
}
Terminal window
curl -X POST http://localhost:8080/orders \
-H "Content-Type: application/json" \
-d '{"items":[{"sku":"WIDGET-1","qty":3},{"sku":"WIDGET-2","qty":1}]}'
{
"first_sku": "WIDGET-1",
"first_sku_bracket": "WIDGET-1",
"first_qty": 3
}
  • Dot and bracket syntax are interchangeable, at every level. body.items.0.sku, body.items[0].sku, and even body.a.0.b.1 (nested arrays, mixed with object fields) all resolve the same way.
  • Object keys win over array indices. A JSON object whose keys happen to look numeric ({"0": "zero"}) still resolves by key lookup first; only an actual array is indexed by position.
  • Out-of-range and non-numeric indices degrade gracefully — an empty string (or null under a json: cast), never a panic — the same as any other missing key.
  • Every typed cast below works on an indexed path: {{number:body.items.0.qty}}, {{json:body.items.0}}, and so on.

The faker source doesn’t read the request at all — it generates a fresh, plausible-looking value on every call, so one mock file can stand in for a whole fixture set.

Template Output
{{faker.uuid}} RFC 4122 v4 UUID, e.g. 3fa85f64-5717-4562-b3fc-2c963f66afa6
{{faker.int}} Random integer in the default range 0..=1000000
{{faker.int min=1 max=100}} Random integer in [1, 100]
{{faker.bool}} true or false
{{faker.name}} Random name, e.g. Priya Novak
{{faker.email}} Slugified random name at example.com, e.g. [email protected]
{{faker.timestamp}} Current UTC time in RFC 3339, e.g. 2026-07-22T16:12:54.481+00:00
{
"method": "GET",
"path": "/faker/user",
"status": 200,
"response": {
"id": "{{faker.uuid}}",
"name": "{{faker.name}}",
"email": "{{faker.email}}",
"age": "{{faker.int min=18 max=99}}",
"verified": "{{faker.bool}}",
"created_at": "{{faker.timestamp}}"
}
}

curl http://localhost:8080/faker/user returns a different value on every call:

{
"id": "9c0f1b6d-2f4e-4a1e-b0a2-6c1d7f3e88a4",
"name": "Priya Novak",
"email": "[email protected]",
"age": "37",
"verified": "true",
"created_at": "2026-07-22T16:12:54.481+00:00"
}
  • Every occurrence resolves independently — two {{faker.uuid}} in one response produce two different UUIDs, and {{faker.email}} is not derived from a {{faker.name}} sitting next to it.
  • Faker values are always rendered as JSON strings, since templates are interpolated into string values — unless you add a typed cast.
  • Malformed arguments degrade to the generator’s defaults instead of failing: {{faker.int min=abc}} and {{faker.int min=100 max=1}} both use the default 0..=1000000 range.
  • An unknown generator (e.g. {{faker.credit_card}}) renders as an empty string, like any other unknown template.
  • Resolved after matching, same as every other source — a {{faker.*}} expression sitting in a matcher is treated as a literal string, not evaluated.

By default every templated value is spliced into a JSON string, even when the source is a number or boolean — {"id": "{{path.id}}"} on GET /users/42 renders {"id": "42"}, not {"id": 42}. That’s a hard failure for a typed client (zod, Go’s encoding/json, Rust’s serde_json, Jackson, …), which rejects a string where a number or object belongs. Prefix any source expression with number:, bool:, or json: to render it as that JSON type instead:

Template Renders as
{{number:path.id}} JSON number
{{bool:query.active}} JSON boolean (true/false)
{{json:body.user}} JSON value parsed from the source (object, array, number, …)
{
"id": "{{number:path.id}}",
"views": "{{number:faker.int min=1 max=999}}",
"active": "{{bool:query.active}}",
"user": "{{json:body.user}}"
}

renders {"id": 42, "views": 317, "active": true, "user": {"name": "alice"}} instead of every field coming back quoted.

  • Whole-string only. A cast applies only when the string is exactly one {{cast:source.key}} expression. "user-{{number:path.id}}" stays plain string interpolation — a number can’t be spliced into the middle of a string and still be a number — and renders "user-42".
  • Failed casts degrade, never panic. {{number:path.slug}} on /users/abc falls back to the plain string "abc", and {{json:query.x}} on a value that isn’t valid JSON falls back the same way.
  • Null handling differs by cast. {{json:body.missing}} on a field that wasn’t sent renders JSON null; {{number:...}} and {{bool:...}} fall back to an empty string on a missing key, same as an unprefixed template.
  • An unrecognized cast word (anything other than number, bool, or json) resolves to an empty string, the same as an unrecognized source.
  • Works everywhere templates work — response, sequence step responses, and a response_file with template: true — and with every source (path, query, header, body, faker).
  • Templates are resolved after the mock (or sequence step) is chosen, so the interpolated value never affects matching itself — you can’t use a template to influence which mock wins.
  • An unknown source, a missing key, or an explicit JSON null all resolve to an empty string. Malformed or unresolvable templates never panic and never leak the raw {{ }} text into the response.
  • Non-string body values (numbers, booleans, nested objects/arrays) render using their JSON text form — {{body.age}} for {"age": 30} produces 30 (unquoted), not "30".
  • A response with no {{ }} expressions is returned unchanged — there’s no templating overhead if you don’t use it.
  • Header lookups are case-insensitive: {{header.X-Request-Id}} and {{header.x-request-id}} are equivalent.
  • {{path.id}} requires the mock’s path to actually capture that parameter. If your path is /users/:id but you reference {{path.slug}}, it silently renders as an empty string — there’s no error, so double-check the parameter name matches.
  • Query values are always strings, same as with query parameter matching. {{query.page}} for ?page=2 renders "2", not 2.
  • Templates only work inside response string values. You can’t template into status, method, path, or matcher fields.