WireMock Test Data: Generate Consistent Mappings and Response Bodies
WireMock test data usually starts as a handful of hand-written stub
files. Someone copies a response from the real API, trims it to two
records, saves it under mappings/, and moves on. A year later the
real API has new fields, the stub still has the old ones, the list stub
returns ids that no detail stub knows about, and nothing ever returns a
404 or a 500. This post replaces those hand-written files with
generated ones. One template with a fixed seed produces realistic list
and detail payloads that share their ids, plus deliberate 404, 422, and
500 bodies. A short script writes them into mappings/ and __files/,
and you commit the result.
JsonFabrica's part is narrow. It's a JSON-over-HTTP generation API with no WireMock extension, plugin, or Docker image. Your script calls the API, writes the JSON it gets back into ordinary WireMock files, and WireMock serves them the same way it serves stubs you wrote by hand, whether it runs standalone, in Docker, or embedded in JUnit tests.
Why hand-written WireMock test data drifts
Hand-written stubs fail in three predictable ways:
- They drift from the real API. A stub body is a snapshot of whatever the response looked like when someone wrote it. Fields get added, renamed, or made nullable, and the stub keeps passing tests against a shape the API no longer returns.
- They cover the happy path only. Typing JSON is tedious, so the
files hold two or three tidy records. Every ticket is assigned, every
list has items, every optional field is set. The client code that
handles
null, empty arrays, and error responses never runs. - Related stubs disagree. The list stub and the detail stubs are separate files written at different times. The list says ticket 42 belongs to "Jane Doe", and the detail stub for 42 says "Test User", or doesn't exist at all.
If you're mocking for a frontend dev server with MSW rather than for JVM tests or a standalone mock server, the mock API response data post covers that setup. The rest of this post is about WireMock.
WireMock mappings JSON: request, response, and __files
A WireMock stub is a JSON mapping file. The request object says what
to match, and the response object says what to return:
{
"priority": 5,
"request": {
"method": "GET",
"urlPath": "/api/tickets/tkt_000001"
},
"response": {
"status": 200,
"headers": { "Content-Type": "application/json" },
"bodyFileName": "tickets/tkt_000001.json"
}
}
A few parts of this format matter for generated stubs:
- Where the files live. On startup, WireMock loads every mapping
file under
mappings/in its root directory. A response can carry its body inline, as a string inbodyor as JSON injsonBody, or reference a file under__files/withbodyFileName. - Bodies in
__files/, rules inmappings/. This post puts every response body in__files/and keeps the mapping files to matching rules, status, and priority. A pull request that changes the data then shows up as changes to body files, and a change to matching shows up inmappings/. - Path matching.
urlPathmatches the path exactly and ignores the query string, so the list stub also answers/api/tickets?page=2.urlPathPatternmatches the path against a regular expression. - Priority. When several stubs match a request, the one with the
lowest
prioritynumber wins, and 1 is the highest priority. The error stubs below rely on this.
The example API is a support desk with three endpoints:
GET /api/tickets for the list, GET /api/tickets/{id} for one
ticket, and POST /api/tickets to create one.
Generate WireMock stub response data from one template
The template produces a single JSON document with two keys. tickets
holds the full records, and errors holds the error bodies. The script
in the next section splits that document into WireMock files.
{
"tickets": [
<for(i, 1, getParam('count', 8))><if(getVar('i') > 1)>,<endIf>
<if(getVar('i') == 1)><setVar('status', 'new')>
<elseIf(getVar('i') == 2)><setVar('status', 'closed')>
<else><setVar('status', getRandomElement('open', 'open', 'pending', 'solved'))>
<endIf>
<setVar('first', getRandomName())><setVar('last', getRandomSurname())>
<setVar('updatedAt', getRandomDate('2026-09-16', '2026-10-01'))>
{
"id": "tkt_<appendBefore('0', getVar('i'), 6)>",
"subject": "<getRandomElement(
'Cannot reset password',
'Invoice shows the wrong VAT rate',
'CSV export times out',
'SSO login redirects in a loop',
'Webhook deliveries fail with 401',
'Add three seats to our team plan',
'Charts do not load in Safari',
'Charged twice for September')>",
"status": "<getVar('status')>",
"priority": "<getRandomElement(
'low',
'normal',
'normal',
'high',
'urgent')>",
"requester": {
"name": "<getVar('first')> <getVar('last')>",
"email":
"<toLowerCase(getVar('first'))>.<toLowerCase(getVar('last'))>@example.com"
},
"assignee": <if(getVar('i') == 1)>null
<else>{
"name": "<getRandomFullName()>",
"team": "<getRandomElement('Billing', 'Platform', 'Support')>"
}<endIf>,
"createdAt": "<getRandomDate('2026-08-01', '2026-09-01')>",
"updatedAt": "<getVar('updatedAt')>",
"closedAt": <if(getVar('status') == 'closed')>"<getVar('updatedAt')>"
<else>null<endIf>,
"comments": [
<if(getVar('i') > 1)>
<for(c, 1, getRandomNumber(1, 3))><if(getVar('c') > 1)>,<endIf>
{
"author": "<getRandomFullName()>",
"body": "<getRandomElement(
'Can you share a screenshot?',
'Reproduced on our side, escalating.',
'A fix is rolling out today.',
'Still happening after the update.',
'Thanks, that worked.')>",
"createdAt": <if(getVar('c') == 1)>
"<getRandomDate('2026-09-01', '2026-09-06')>"
<elseIf(getVar('c') == 2)>
"<getRandomDate('2026-09-06', '2026-09-11')>"
<else>
"<getRandomDate('2026-09-11', '2026-09-16')>"<endIf>
}<end_for>
<endIf>
]
}<end_for>
],
"errors": {
"notFound": {
"error": {
"code": "ticket_not_found",
"message": "Ticket {{request.pathSegments.[2]}} does not exist",
"requestId": "req_<getRandomNumber(10000000, 99999999)>"
}
},
"validation": {
"error": {
"code": "validation_failed",
"message": "The request body is invalid",
"fields": [{ "field": "subject", "issue": "must not be empty" }],
"requestId": "req_<getRandomNumber(10000000, 99999999)>"
}
},
"server": {
"error": {
"code": "internal_error",
"message": "Something went wrong on our side",
"requestId": "req_<getRandomNumber(10000000, 99999999)>"
}
}
}
}
How it works:
- The ticket count is a parameter.
getParamreadscountfrom the request and falls back to 8. The loop end is inclusive, the loop variable is read withgetVar('i'), and the comma guard<if(getVar('i') > 1)>,<endIf>puts a comma before every ticket except the first. The control flow docs coverforandif/elseIf/else. - Ids come from the loop index.
appendBeforepads the index with zeros, so the ids aretkt_000001throughtkt_000008and stay the same on every run. A sequence would be the wrong tool here: sequences are durable and keep counting across calls, so regenerating the file would hand out new ids. - The first tickets are planted edge cases. Ticket 1 is
new, has no assignee (null), and has an emptycommentsarray, which is the state hand-written stubs rarely include. Ticket 2 isclosed, so it has aclosedAt. The rest get a random status, withopenlisted twice ingetRandomElementto make it twice as likely as each other status. - Variables only where a value is used twice.
statusis printed and also decides whetherclosedAtis set.firstandlastbuild both the requester's name and their email, so the two match.updatedAtis printed and reused asclosedAton a closed ticket. Everything else is a function call or anifwritten directly in its field. - Dates stay in order without arithmetic. Template expressions
support comparisons and
&&/||, but no arithmetic, so a date can't be "created plus three days". Non-overlapping ranges forgetRandomDatedo the same job: tickets are created in August, comments are posted in the first half of September in order (comment 1 before comment 2 before comment 3), andupdatedAtfalls in the second half. - Line breaks sit outside string values. Where a field's
ifspans several lines, each branch carries its own quotes, so the extra whitespace lands between JSON tokens and the output still parses. Inside a placeholder you can break the line between arguments, but not inside a quoted string, and never outside the parentheses. That's why the longgetRandomElementlists put one argument per line.
The {{request.pathSegments.[2]}} in the 404 message isn't a mistake.
It's WireMock's own templating syntax, and the section on
response templating
explains why it passes through the generator untouched.
Write the mappings and __files with one script
Save the template as scripts/tickets.tmpl. The script below sends it
to POST /v1/templates/generate with a
fixed seed, checks the seed came back, and writes the WireMock files
with jq:
#!/usr/bin/env bash
# scripts/wiremock-stubs.sh: regenerate the tickets API stubs on demand.
set -euo pipefail
SEED=20261006 # change only when you mean to replace the data
COUNT=8
ROOT=wiremock # WireMock root directory: mappings/ and __files/
OUT=$(mktemp)
trap 'rm -f "$OUT"' EXIT
jq -n --rawfile body scripts/tickets.tmpl \
--argjson seed "$SEED" --argjson count "$COUNT" \
'{body: $body, seed: $seed, params: {count: $count}}' \
| curl -sS --fail -X POST https://api.jsonfabrica.com/v1/templates/generate \
-H "Authorization: Bearer $JSONFABRICA_API_KEY" \
-H "Content-Type: application/json" \
-d @- -o "$OUT"
# Stop if the seed wasn't applied: nobody could regenerate the files.
jq -e --argjson s "$SEED" '.meta.seed == $s' "$OUT" > /dev/null
# Start clean, so stubs for tickets that no longer exist don't linger.
rm -f "$ROOT"/mappings/tickets-*.json
rm -rf "$ROOT/__files/tickets"
mkdir -p "$ROOT/mappings" "$ROOT/__files/tickets"
# body <file> <jq filter> [jq args]: write a body to __files/tickets/
body() {
local file=$1 filter=$2
shift 2
jq "$@" "$filter" "$OUT" > "$ROOT/__files/tickets/$file"
}
# get <path>: request matcher for a GET on an exact path
get() { jq -nc --arg p "$1" '{method: "GET", urlPath: $p}'; }
# stub <name> <priority> <request> <status> <body file> [templated]
stub() {
jq -n --argjson p "$2" --argjson req "$3" --argjson s "$4" \
--arg f "tickets/$5" --argjson tpl "${6:-false}" '
{
priority: $p,
request: $req,
response: {
status: $s,
headers: {"Content-Type": "application/json"},
bodyFileName: $f
}
}
| if $tpl then .response.transformers = ["response-template"]
else . end' > "$ROOT/mappings/tickets-$1.json"
}
# List: summaries of exactly the records the detail stubs serve.
body list.json '{
data: [.data.tickets[] | {id, subject, status, priority,
requesterName: .requester.name, updatedAt}],
total: (.data.tickets | length)
}'
stub list 5 "$(get /api/tickets)" 200 list.json
# Detail: one body file and one stub per ticket, same id in both.
for id in $(jq -r '.data.tickets[].id' "$OUT"); do
body "$id.json" '.data.tickets[] | select(.id == $id)' --arg id "$id"
stub "$id" 5 "$(get "/api/tickets/$id")" 200 "$id.json"
done
# 500: the third ticket is in the list, but its detail call fails.
FAIL_ID=$(jq -r '.data.tickets[2].id' "$OUT")
body error-500.json '.data.errors.server'
stub error-500 1 "$(get "/api/tickets/$FAIL_ID")" 500 error-500.json
# 404: any other ticket id. WireMock fills in the id per request.
body error-404.json '.data.errors.notFound'
stub error-404 10 \
'{"method": "GET", "urlPathPattern": "/api/tickets/[^/]+"}' \
404 error-404.json true
# 422: creating a ticket with an empty subject.
body error-422.json '.data.errors.validation'
stub error-422 1 '{
"method": "POST",
"urlPath": "/api/tickets",
"bodyPatterns": [
{"matchesJsonPath": {"expression": "$.subject", "equalTo": ""}}
]
}' 422 error-422.json
echo "wrote $(ls "$ROOT"/mappings/tickets-*.json | wc -l) stubs"
The generate response has two fields: data, the generated document
already parsed as JSON, and meta, whose seed echoes the seed that
was used. The seed check matters. If a typo drops the seed, the API
picks a random one, and without the check you'd commit stubs nobody can
regenerate. If you'd rather save the template in JsonFabrica, create it
once with POST /v1/templates and call
POST /v1/templates/{templateId}/generate with the same seed and
params instead.
The script writes 12 stubs and 12 body files:
wiremock/
mappings/
tickets-list.json
tickets-tkt_000001.json ... tickets-tkt_000008.json
tickets-error-404.json
tickets-error-422.json
tickets-error-500.json
__files/tickets/
list.json
tkt_000001.json ... tkt_000008.json
error-404.json
error-422.json
error-500.json
Keep list and detail stubs consistent
The list body isn't generated separately. jq projects it from the same
tickets array the detail bodies are cut from, so every row in
list.json has a detail file with the same id, subject, status, and
requester, and total is computed from the array length. A field that
exists in both places can't disagree, and adding a ticket can't
produce a list row with no detail stub.
The template can't do this projection itself. It renders the document top to bottom, so a second loop over the tickets would draw new random names rather than repeat the old ones. Generating the full records once and deriving every view from them in the script is the reliable way to cross-reference stubs.
Error stubs: 404, 422, and 500 with WireMock priority
The error stubs lean on WireMock's priority rule, lowest number wins:
| Stub | Priority | Matches |
|---|---|---|
error-500, error-422 |
1 | One ticket's detail, or a POST with an empty subject |
list, tkt_000001 ... tkt_000008 |
5 | The list and each known ticket |
error-404 |
10 | GET /api/tickets/ plus any id |
tkt_000003 is matched by two stubs, its detail stub and error-500.
The 500 wins on priority, so that ticket appears in the list, but
opening it fails, which is exactly the case to test in a client that
loads a list and then fetches details. Every id that no specific stub
matches falls through to the 404. A POST with a non-empty subject
matches none of these stubs, and WireMock answers with its own
"request was not matched" 404. Add a 201 stub the same way if your
tests create tickets.
What the generated WireMock stubs return
These bodies come from the template and script above, rendered with
seed 20261006 and served by WireMock 3.13.1 standalone. The first two
rows of __files/tickets/list.json:
{
"data": [
{
"id": "tkt_000001",
"subject": "Cannot reset password",
"status": "new",
"priority": "high",
"requesterName": "Adella Reynolds",
"updatedAt": "2026-09-24T08:05:13.096Z"
},
{
"id": "tkt_000002",
"subject": "SSO login redirects in a loop",
"status": "closed",
"priority": "low",
"requesterName": "Gilen Dean",
"updatedAt": "2026-09-24T15:32:05.728Z"
}
],
"total": 8
}
The list continues to tkt_000008. GET /api/tickets/tkt_000001
returns the planted empty state, with no assignee and no comments:
{
"id": "tkt_000001",
"subject": "Cannot reset password",
"status": "new",
"priority": "high",
"requester": {
"name": "Adella Reynolds",
"email": "[email protected]"
},
"assignee": null,
"createdAt": "2026-08-30T13:35:13.112Z",
"updatedAt": "2026-09-24T08:05:13.096Z",
"closedAt": null,
"comments": []
}
GET /api/tickets/tkt_000002 returns the closed ticket, whose
closedAt equals its updatedAt:
{
"id": "tkt_000002",
"subject": "SSO login redirects in a loop",
"status": "closed",
"priority": "low",
"requester": {
"name": "Gilen Dean",
"email": "[email protected]"
},
"assignee": {
"name": "Pauline Hawman",
"team": "Support"
},
"createdAt": "2026-08-10T01:36:05.115Z",
"updatedAt": "2026-09-24T15:32:05.728Z",
"closedAt": "2026-09-24T15:32:05.728Z",
"comments": [
{
"author": "Fenin Halefield",
"body": "Can you share a screenshot?",
"createdAt": "2026-09-02T04:20:11.715Z"
},
{
"author": "Willow Langman",
"body": "A fix is rolling out today.",
"createdAt": "2026-09-08T23:51:46.764Z"
}
]
}
GET /api/tickets/tkt_000003 returns the 500 body with status 500,
even though that ticket is in the list. GET /api/tickets/tkt_999999
returns 404 with the requested id filled in:
{
"error": {
"code": "ticket_not_found",
"message": "Ticket tkt_999999 does not exist",
"requestId": "req_22954472"
}
}
Serve the stubs: standalone, Docker, or the JUnit 5 extension
The generated files are ordinary WireMock files, so every way of
running WireMock picks them up without changes. In each case, point
WireMock at the directory that contains mappings/ and __files/.
Standalone JAR. Pass the directory with --root-dir:
java -jar wiremock-standalone-3.13.1.jar --port 8080 --root-dir wiremock
Docker. The official wiremock/wiremock image reads its root
directory from /home/wiremock, so mount the generated directory there.
Pin the image tag to the WireMock version you test against:
docker run --rm -p 8080:8080 \
-v "$PWD/wiremock:/home/wiremock" \
wiremock/wiremock:3.13.1
JUnit 5. The WireMockExtension starts an embedded server per test
class. By default it looks for mappings/ and __files/ under
src/test/resources, so either generate into that directory or point
the extension at yours:
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
import static org.junit.jupiter.api.Assertions.*;
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
class TicketClientTest {
@RegisterExtension
static WireMockExtension api = WireMockExtension.newInstance()
.options(wireMockConfig().dynamicPort()
.usingFilesUnderDirectory("wiremock"))
.build();
// TicketClient and its types stand in for your own client code.
TicketClient client() {
return new TicketClient(api.baseUrl());
}
@Test
void detailMatchesItsListRow() {
TicketSummary row = client().list().get(1);
Ticket ticket = client().get(row.id());
assertEquals(row.requesterName(), ticket.requester().name());
assertNotNull(ticket.closedAt());
}
@Test
void unassignedTicketHasNoAssignee() {
assertNull(client().get("tkt_000001").assignee());
}
@Test
void failedDetailCallKeepsTheRequestId() {
TicketApiException e = assertThrows(TicketApiException.class,
() -> client().get("tkt_000003"));
assertEquals(500, e.status());
assertEquals("req_11088180", e.requestId());
}
}
The last test asserts on an exact request id. That only works because the value sits in a committed file and doesn't change between runs.
CI needs no JsonFabrica API key for any of this. The stubs are files in the repository, and the data changes only when someone reruns the script and commits the result. For a broader look at wiring generation into scripts and pipelines, see API-first data generation.
WireMock response templating vs generated data
WireMock has its own way to produce dynamic responses: response
templating with Handlebars. A stub opts in by listing
response-template in its transformers, as the 404 stub above does.
In WireMock 3.x that per-stub opt-in works without a startup flag, and
--global-response-templating applies templating to every stub
instead. Check the WireMock docs for the version you run, since
templating setup has changed between major versions.
Templating is the right tool for two jobs:
- Echoing the request. Helpers such as
{{request.pathSegments.[2]}},{{request.query.page}}, and{{jsonPath request.body '$.subject'}}put parts of the request into the response. A "created" response can return the subject the client just sent. A generated file can't do that, because it doesn't know the request. - Per-request randomness. Helpers like
randomValueandrandomIntreturn a fresh value on every call, which suits fields nobody asserts on, such as a request id that only needs to look unique.
Generated files are the better fit when the data itself is what you're testing:
- Reproducible. WireMock doesn't document a way to seed its random helpers, so a value that broke a test is gone after the run. A committed body file has the same values on every run, so a test can assert on them and a failure can be replayed.
- Reviewable. A change to the data is a diff in a pull request, next to the code change that needed it.
- Cross-referenced. Each stub is templated independently, per request. A list stub that templates random ids has no way to make a separate detail stub return the same records. With generated files, the list and the details are cut from one document.
- Realistic and correlated. The template ties fields together:
the email matches the requester's name,
closedAtis set only on a closed ticket, and comment dates fall in order.
You don't have to pick one. The 404 stub in this post combines them:
its body, error code, and request id are generated and committed, and
{{request.pathSegments.[2]}} lets WireMock fill in the requested id
per request. If you'd rather have a new request id on every 404, swap
req_22954472 for {{randomValue type='UUID'}} in the template and
give up the fixed value for that one field.
The two syntaxes don't collide because they use different delimiters.
A JsonFabrica placeholder is a function call in angle brackets, such as
<getRandomFullName()>. WireMock's Handlebars expressions use double
braces. The generator treats {{request.pathSegments.[2]}} as plain
text and copies it into the output unchanged, and by the time WireMock
reads the body file, no angle-bracket placeholders are left in it.
Keeping generated WireMock stubs reproducible
The same template, seed, and parameters always produce the same files.
One change to understand before you make it: raising count adds
tickets at the end. With seed 20261006, count: 10 returns the same
first 8 tickets as count: 8, plus two new ones. The error bodies are
drawn after the tickets, so their request ids change, and a test that
asserts on req_11088180 needs updating.
When the real API changes shape, update the template, rerun the script,
and review the diff in __files/. The diff shows every stub the change
touches. Keeping the template in step with the real API is still a
manual job. If several services need to agree on the same payload
shapes, test data for contract testing
covers that problem. For seeds and determinism in general, including
how to replay an unseeded run, see
reproducible test data from seeds.
The same seeded-file approach also works for API test runners, as in
Postman test data for Newman.
FAQ
What is the difference between mappings and __files in WireMock?
The mappings directory holds stub mapping files: JSON documents that
pair a request matcher with a response definition such as status,
headers, and body. The __files directory holds response bodies that a
mapping references by path with bodyFileName. Keeping bodies in
__files keeps the mapping files short and lets you review payload
changes separately from matching rules.
Can WireMock generate random response data?
Yes. With response templating enabled for a stub, Handlebars helpers
such as randomValue and randomInt produce a new value on every
request, and other helpers can echo parts of the request back. WireMock
doesn't document a way to seed those helpers, so the values can't be
replayed after a failure. For data you want to review, assert on, and
reproduce, generate the bodies into files from a fixed seed and commit
them.
How do I return a 500 error for one specific request in WireMock?
Add a second stub that matches that exact request and returns status 500, and give it a higher priority than the normal stub. In WireMock, priority 1 is the highest, so a 500 stub with priority 1 wins over a detail stub with priority 5 for the same URL. A catch-all stub with a low priority such as 10 can then return 404 for every id that no other stub matches.
Can I use WireMock response templating together with generated response bodies?
Yes. A generated body file can contain Handlebars expressions such as
{{request.pathSegments.[2]}}, and WireMock fills them in per request
when the stub lists response-template in its transformers.
JsonFabrica placeholders use angle brackets, like
<getRandomFullName()>, so the engine copies the double-brace
expressions through unchanged and the two syntaxes don't collide.
Does JsonFabrica have a WireMock extension or integration?
No. JsonFabrica is an HTTP API that returns generated JSON. It has no
WireMock extension, plugin, or Docker image. A script calls the API
with a fixed seed, writes the response bodies into __files and the
stub definitions into mappings, and WireMock serves those committed
files like any other stubs.
One template, one fixed seed, and a short script give WireMock list and detail stubs that agree with each other, plus the empty states and error responses hand-written stubs leave out. Seeded, parameterized generation is part of the JsonFabrica API, and the templates API reference documents the generate request and response.
Generate realistic test data with JsonFabrica
Describe the shape of your data once, then generate as many fresh, realistic JSON documents as you need via a simple API call.