WireMock’s matching model is built around a small set of pattern types that you reuse everywhere: URLs, headers, query parameters, and bodies all use variations of the same abstractions. In this article we’ll build an intuition for those abstractions and then see them in a Java example.
The Core Abstractions: ContentPattern and StringValuePattern
At the heart of WireMock matching is the idea “given some content, decide how well it matches this expectation.” That idea is embodied in ContentPattern and its concrete subclasses.
- ContentPattern
is the conceptual base: “match content of type T and produce a MatchResult.” - StringValuePattern is the concrete type for matching strings and underpins almost every simple matcher you use in the DSL.
You rarely construct StringValuePattern directly; you use the DSL factory methods that create it for you. These include:
equalTo("foo")– exact match.containing("foo")– substring.matching("[A-Z]+")– regular expression.equalToJson("{...}"),equalToXml("<root/>")– structural equality for JSON/XML.
Rationale: by funnelling everything through StringValuePattern, WireMock can reuse the same matching semantics across URLs, headers, query params, and body fragments, and can assign a distance score that lets it pick the best matching stub.
Mini example (headers and query):
stubFor(get(urlPathEqualTo("/search"))
.withQueryParam("q", equalTo("WireMock"))
.withHeader("Accept", containing("json"))
.willReturn(okJson("""{ "result": "ok" }"""))
);
Both equalTo("WireMock") and containing("json") are StringValuePattern instances internally.
MultiValuePattern: Matching Lists of Values
Headers and query parameters can have multiple values; sometimes you care about the set of values as a whole, not just any one of them. That’s what MultiValuePattern is for.
Conceptually, MultiValuePattern answers: “does this list of strings satisfy my condition?” Typical usage for query parameters:
stubFor(get(urlPathEqualTo("/items"))
.withQueryParam("tag", havingExactly("featured", "sale"))
.willReturn(ok())
);
Here:
havingExactly("featured", "sale")expresses that the parametertagmust have exactly those two values and no others.- MultiValuePattern is the pattern over that list, analogous to how StringValuePattern is a pattern over a single string.
Rationale: this is important for APIs where order and multiplicity of query or header values matter (e.g. “all of these scopes must be present”). Without a dedicated multi-value abstraction you would be forced to encode such logic in brittle string hacks.
UrlPathPattern and URL-Level Matching
WireMock lets you match URLs at several levels in the Java DSL: exact string, regex, or path-only vs path+query. UrlPathPattern corresponds to urlPathMatching(...) when you use the Java client.
Key URL matchers in Java:
urlEqualTo("/path?query=...")– exact match on full path (with query).urlMatching("regex")– regex on full path (with query).urlPathEqualTo("/path")– exact match on path only.urlPathMatching("regex")– regex on path only.
stubFor(get(urlPathMatching("/users/([A-Za-z0-9_-]+)/repos"))
.willReturn(aResponse()
.withStatus(200)));
In modern WireMock 3 you’ll also see templates like urlPathTemplate("/contacts/{contactId}"), which are easier to read and type-safe at the path level:
stubFor(get(urlPathTemplate("/contacts/{contactId}"))
.willReturn(aResponse()
.withStatus(200)));
Rationale: splitting “path” from “query” and using distinct matchers lets you:
- Use exact matches (
urlEqualTo,urlPathEqualTo) when you want deterministic behaviour (faster, simpler). - Only fall back to regex (
urlMatching,urlPathMatching) when you truly need flexible matching, such as versioned paths or dynamic IDs.
MatchesJsonPathPattern: Querying Inside JSON Bodies
When the request body is JSON and you care about conditions inside it rather than full equality, WireMock uses MatchesJsonPathPattern. You access it via matchingJsonPath(...) in the DSL.
Two main flavours:
matchingJsonPath("$.message")– body must match the JSONPath (i.e. the expression finds at least one node).matchingJsonPath("$.message", equalTo("Hello"))– evaluates JSONPath, converts the result to a string, then matches it with a StringValuePattern.
Example:
stubFor(post(urlEqualTo("/api/message"))
.withRequestBody(matchingJsonPath("$.message", equalTo("Hello World!")))
.willReturn(aResponse().withStatus(200))
);
Important detail: all WireMock matchers operate on strings, so the JSONPath result is stringified before applying the StringValuePattern. This even allows selecting a sub-document and then matching it with equalToJson(...).
Rationale: JSONPath is ideal when you need to assert that some field exists or meets a condition (e.g. price > 10) without tightly coupling your tests to the entire JSON shape. It makes tests robust to benign changes in unrelated fields.
JsonUnit Placeholders in equalToJson
When you do want structural equality for JSON, equalToJson is your tool, and it is powered by JsonUnit. JsonUnit supports placeholders, which WireMock exposes to let you relax parts of the JSON equality check.
You embed placeholders directly into the expected JSON:
stubFor(post(urlEqualTo("/orders"))
.withRequestBody(equalToJson("""
{
"id": "${json-unit.any-string}",
"amount": 123.45,
"status": "NEW",
"metadata": "${json-unit.ignore}"
}
"""))
.willReturn(ok())
);
Common placeholders:
${json-unit.ignore}– ignore value and type, just require key presence.${json-unit.any-string}– value can be any string.${json-unit.any-number}– any number.${json-unit.any-boolean}– any boolean.${json-unit.regex}[A-Z]+– value must match this regex.
You can also change delimiters if ${ and } clash with your payload conventions.
Rationale: equalToJson is excellent for enforcing payload shape and fixed fields, but brittle when some fields are inherently variable (IDs, timestamps, correlation IDs). JsonUnit placeholders let you keep strictness where it matters and loosen it where variability is expected.
JSON Schema Matching
For more formal validation you can use JSON Schema with matchers like matchingJsonSchema. This checks that the JSON body (or a path variable) conforms to an explicit schema: types, required fields, min/max, etc.
Typical body matcher:
stubFor(post(urlEqualTo("/things"))
.withRequestBody(
matchingJsonSchema("""
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "string", "minLength": 2 },
"name": { "type": "string" },
"price": { "type": "number", "minimum": 0 }
},
"additionalProperties": false
}
""")
)
.willReturn(created())
);
You can also apply matchingJsonSchema to path parameters when you use path templates, e.g. constraining userId to a specific shape.
Rationale:
- JsonPath answers “does there exist an element meeting this condition?”
- equalToJson + JsonUnit placeholders answers “is this JSON equal to a template with some flexible parts?”
- JSON Schema answers “does this JSON globally satisfy a formal contract?”.
JSON Schema is especially useful when your WireMock stub is acting as a consumer-driven contract test for another service.
Custom Matcher: Escaping the Built-in Model
When WireMock’s built-in matchers are not enough, you can implement Custom Matchers. The main extension is RequestMatcherExtension, which gives you full control over how a request is matched.
The minimal shape:
import com.github.tomakehurst.wiremock.extension.Parameters;
import com.github.tomakehurst.wiremock.extension.requestfilter.RequestMatcherExtension;
import com.github.tomakehurst.wiremock.http.MatchResult;
import com.github.tomakehurst.wiremock.http.Request;
public class BodyLengthMatcher extends RequestMatcherExtension {
@Override
public String getName() {
return "body-too-long";
}
@Override
public MatchResult match(Request request, Parameters parameters) {
int maxLength = parameters.getInt("maxLength");
boolean tooLong = request.getBody().length > maxLength;
return MatchResult.of(tooLong);
}
}
You then reference it by name in your stub:
stubFor(requestMatching("body-too-long", Parameters.one("maxLength", 2048))
.willReturn(aResponse().withStatus(422))
);
There is also a lower‑level ValueMatcher<T> interface that you can use in verification or in some advanced APIs to match arbitrary values, without registering a global extension.
Rationale: custom matchers are your “escape hatch” for domain‑specific logic which would otherwise be contorted into regexes or JSONPath. Examples include:
- Checking that a JWT in a header is valid and contains certain claims.
- Ensuring consistency between header and body fields.
Putting It All Together
Below is a Java 17 style example which combines several of the discussed matcher types. You can drop this into a plain Maven/Gradle project that has wiremock dependencies;
Example: Product API Stub with Multiple Matchers
import com.github.tomakehurst.wiremock.WireMockServer;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
public class ProductApiStub {
public static void main(String[] args) {
WireMockServer server = new WireMockServer(
wireMockConfig().port(8080)
);
server.start();
configureFor("localhost", 8080);
registerStubs(server);
Runtime.getRuntime().addShutdownHook(new Thread(server::stop));
System.out.println("WireMock running at http://localhost:8080");
}
private static void registerStubs(WireMockServer server) {
server.stubFor(post(urlPathTemplate("/shops/{shopId}/products"))
// UrlPathTemplate + JSON Schema for path parameter
.withPathParam("shopId", matchingJsonSchema("""
{
"type": "string",
"minLength": 3,
"maxLength": 10
}
"""))
// MultiValuePattern on query param
.withQueryParam("tag", havingExactly("featured", "sale"))
// StringValuePattern: header must match regex
.withHeader("X-Request-Id", matching("req-[0-9a-f]{8}"))
// JsonUnit placeholders in equalToJson
.withRequestBody(equalToJson("""
{
"id": "${json-unit.any-string}",
"name": "Socks",
"price": 9.99,
"metadata": "${json-unit.ignore}"
}
"""))
// MatchesJsonPathPattern + StringValuePattern
.withRequestBody(matchingJsonPath("$[?(@.price > 0)]"))
.willReturn(okJson("""
{
"status": "ACCEPTED",
"message": "Product created"
}
""")));
}
}
What this stub requires:
- Path:
/shops/{shopId}/productswhereshopIdis a string 3–10 chars long (validated via JSON Schema). - Query:
?tag=featured&tag=saleand only those values (MultiValuePattern). - Header:
X-Request-Idmust matchreq-[0-9a-f]{8}(regex StringValuePattern). - Body: JSON with fixed fields
nameandprice, but any string foridand anything formetadata, thanks to JsonUnit placeholders. - Body: JSONPath asserts that
$.priceis strictly greater than 0.
How to Validate the Example
Using any HTTP client (curl, HTTPie, Postman, Java HTTP client), send:
curl -i \
-X POST "http://localhost:8080/shops/abc/products?tag=featured&tag=sale" \
-H "Content-Type: application/json" \
-H "X-Request-Id: req-deadbeef" \
-d '{
"id": "any-random-id",
"name": "Socks",
"price": 9.99,
"metadata": { "color": "blue" }
}'
You should receive a 200 OK with JSON body:
{
"status": "ACCEPTED",
"message": "Product created"
}
If you break one constraint at a time (e.g. price negative, missing tag value, bad X-Request-Id format, too-short shopId), the request will no longer match that stub, and you will either hit no stub or some other fallback stub, which is precisely how you verify each matcher behaves as expected.
Leave a Reply