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.
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.
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
-
pathSegmentsis zero-indexed including the leading empty segment. For/orders/ord_1, segment0isordersand segment1isord_1. Off-by-one here yields a body whose id is the literal stringorders, which looks like a data bug rather than a template bug. -
jsonBodyvalues 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 usejsonBodywith abodystring 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
requestblock, and near-miss diagnosis via the request journal remains the way to work out why a stub was skipped.
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.
Related
- Setting Up WireMock with Spring Boot — the same templating driven from a JVM test
- Running WireMock in Docker Compose — where the templating flag is set
- Paginating Mock List Endpoints — when to move past templating into code
← Back to WireMock Standalone Configuration