Templating Dynamic WireMock Responses

You have forty stub files that differ only in an order id. Templating collapses them into one: the response is built from the request, so GET /orders/ord_0007 returns an order whose id is ord_0007 without anyone writing a file for it.

Context: static stubs multiply

A static WireMock mapping pairs one request pattern with one fixed body. The moment a test needs a second identifier, that pairing forces a second file — and the two files immediately begin to drift, because a schema change has to be applied to both.

Templating breaks the coupling. The request pattern stays a pattern, and the response body references the parts of the request that matter. One mapping then covers an unbounded set of identifiers, and there is one place to update when the shape changes.

The cost is that a templated response cannot disagree with the request, which is exactly what makes some tests worthless. If the assertion is “the page shows the id it asked for”, a template guarantees it passes. Use templating for the parts that genuinely vary and fixed values for the parts under test.

Static stubs multiply; one template does not On the left, a stack of separate mapping files each pinned to one order id, with a note that a schema change must be applied to all of them. On the right, a single templated mapping matching a path pattern, with the response body referencing the request path segment, covering every identifier from one file. One file per identifier get-order-ord_0001.json → { "id": "ord_0001", … } get-order-ord_0002.json → { "id": "ord_0002", … } get-order-ord_0003.json → { "id": "ord_0003", … } … thirty-seven more … A schema change touches forty files. Two of them already disagree and nobody knows which. One templated mapping urlPathPattern: /orders/[^/]+ "id": "{{request.pathSegments.[1]}}" "status": "paid" one file, every identifier A schema change touches one file. Nothing can drift, because there is nothing to drift from.

Solution

1. Enable the transformer

Templating is opt-in. Either turn it on globally:

# docker-compose.yml
services:
  wiremock:
    image: wiremock/wiremock:3.13.2
    command: ["--global-response-templating", "--disable-banner"]
    ports: ["8080:8080"]
    volumes:
      - ./wiremock/mappings:/home/wiremock/mappings:ro
      - ./wiremock/__files:/home/wiremock/__files:ro

Or name it per stub, which is preferable when only a few mappings need it:

{
  "response": {
    "transformers": ["response-template"]
  }
}

Forgetting this step produces the single most common symptom in this area: a response body containing the literal text {{request.pathSegments.[1]}}.

2. Echo the request into the response

{
  "request": {
    "method": "GET",
    "urlPathPattern": "/orders/[^/]+"
  },
  "response": {
    "status": 200,
    "headers": { "Content-Type": "application/json" },
    "transformers": ["response-template"],
    "jsonBody": {
      "id": "{{request.pathSegments.[1]}}",
      "status": "{{#if request.query.status}}{{request.query.status}}{{else}}paid{{/if}}",
      "currency": "{{#if request.query.currency}}{{request.query.currency}}{{else}}GBP{{/if}}",
      "totalMinor": 4250,
      "customer": {
        "id": "cus_{{request.pathSegments.[1]}}",
        "email": "{{request.pathSegments.[1]}}@example.invalid"
      }
    }
  }
}

The {{#if}} guards are not decoration. A bare {{request.query.status}} with no parameter present interpolates an empty string, producing "status": "" — a value the client’s type system says is impossible, so the failure surfaces somewhere unrelated. Every interpolated value that is not guaranteed present needs a default.

3. Template from a request body

For writes, echo the submitted payload back with server-assigned fields added:

{
  "request": {
    "method": "POST",
    "urlPath": "/orders",
    "headers": { "Content-Type": { "contains": "application/json" } }
  },
  "response": {
    "status": 201,
    "headers": {
      "Content-Type": "application/json",
      "Location": "/orders/{{jsonPath request.body '$.reference'}}"
    },
    "transformers": ["response-template"],
    "jsonBody": {
      "id": "{{jsonPath request.body '$.reference'}}",
      "status": "pending",
      "totalMinor": "{{jsonPath request.body '$.totalMinor'}}",
      "currency": "{{jsonPath request.body '$.currency'}}",
      "createdAt": "2026-07-31T09:00:00Z"
    }
  }
}

Note createdAt is a fixed timestamp rather than {{now}}. A response that changes on every call cannot be snapshot-compared and turns a reproducible failure into an intermittent one — the same argument keeping snapshot tests stable with fixed seeds makes about clocks.

4. Template pagination

Pagination is where templating earns the most, because the values are pure functions of the request:

{
  "request": {
    "method": "GET",
    "urlPath": "/orders"
  },
  "response": {
    "status": 200,
    "headers": { "Content-Type": "application/json" },
    "transformers": ["response-template"],
    "jsonBody": {
      "items": [],
      "limit": "{{#if request.query.limit}}{{request.query.limit}}{{else}}20{{/if}}",
      "offset": "{{#if request.query.offset}}{{request.query.offset}}{{else}}0{{/if}}",
      "total": 87
    }
  }
}

For anything beyond echoing — actually slicing a dataset by the offset — templating runs out and a code-level mock is the better tool, as covered in paginating mock list endpoints.

Template the varying parts, fix the asserted parts A four-row table splitting response fields into templated and fixed. Identifiers, echoed filters and pagination parameters should be templated because they vary predictably with the request. Status values, timestamps and the fields under assertion should be fixed, because a templated value cannot contradict the request and therefore cannot fail. Field Treatment Reason resource identifier template one mapping covers every id echoed filter / pagination template with a default absent parameter must not become "" timestamps fixed {{now}} breaks snapshot comparison the field under assertion fixed an echo can never contradict the request The last row is the trap: a fully templated response makes every assertion about it tautological.

Verification

# The identifier is echoed, and the default applies when no query is supplied
curl -s http://localhost:8080/orders/ord_0007 | jq '{ id, status, email: .customer.email }'
# → { "id": "ord_0007", "status": "paid", "email": "[email protected]" }

# The query parameter overrides the default
curl -s 'http://localhost:8080/orders/ord_0007?status=shipped' | jq -r '.status'   # shipped

# No unrendered handlebars survived anywhere
curl -s http://localhost:8080/orders/ord_0007 | grep -q '{{' && echo 'TEMPLATING OFF' || echo 'ok'

The last check belongs in the smoke suite. A configuration change that drops --global-response-templating otherwise produces bodies full of handlebars that the client parses as ordinary strings, and the failures appear far from the cause.

Gotchas and edge cases

  • pathSegments is zero-indexed including the leading empty segment. For /orders/ord_1, segment 0 is orders and segment 1 is ord_1. Off-by-one here yields a body whose id is the literal string orders, which looks like a data bug rather than a template bug.

  • jsonBody values must be strings to be templated. WireMock templates the rendered body, so a numeric field written as "totalMinor": {{jsonPath …}} is invalid JSON in the mapping file itself. Write the placeholder as a quoted string and accept that the response field is a string, or use jsonBody with a body string containing the whole rendered JSON when types matter.

  • Templating runs after matching, so it cannot influence which stub is chosen. A template that would produce a different response for a different query does not make the mapping match more broadly. Matching is still governed entirely by the request block, and near-miss diagnosis via the request journal remains the way to work out why a stub was skipped.


The helpers worth knowing Four helpers. Path segments echo an identifier. Query lookups echo a filter. A JSON path helper reads the request body. And the now helper produces the current time, which is the one to avoid because it makes every response different. request.pathSegments echo an identifier from the path zero-indexed, including the leading segment request.query echo a filter or pagination value always guard it with a default jsonPath request.body read a field from the submitted payload the basis of an echoed create now the current time — avoid it breaks snapshot comparison and reproducibility The first three make one mapping cover many cases; the fourth makes every response unrepeatable.

Knowing when to leave templating behind

Templating is a lever with a clear range. It collapses near-identical stubs into one, and it stops paying the moment the response depends on anything the request does not contain.

The boundary is easy to recognise in practice. As soon as a template needs a conditional inside a conditional, or a loop, or arithmetic on a value, the mapping has become a program written in a language chosen for interpolation. It will be harder to read than the equivalent ten lines of code, harder to test, and impossible to step through when it produces the wrong answer.

Three signals that the boundary has been crossed:

The response depends on prior requests. Templating has no memory. A create that a later read must return needs state, and state means either WireMock scenarios or a code-level mock — not a template.

The response requires computation. Slicing a dataset by offset, summing line items, or deriving a total from a body is arithmetic. WireMock’s helpers can do some of it, and the result is consistently harder to maintain than the code it replaces.

The template exceeds the body it produces. When the handlebars expressions are longer than the JSON around them, the mapping is no longer a stub with a couple of substitutions. That is the point to move the endpoint into a code-level handler and leave the rest of the mappings alone.

There is no requirement to move everything at once. A stack can perfectly well serve ninety per cent of its endpoints from templated mappings and the remaining ten from a small code-level mock behind the same gateway. The mistake is deciding that because templating handled the first ninety, it must handle the last ten too — that decision is how a mapping file becomes something nobody will touch.

← Back to WireMock Standalone Configuration