Quick answer: When two XML or SOAP bodies look nearly identical, matching on the whole body is brittle, while XPath lets you target the specific element that distinguishes the requests. Define a matcher that reads a named node or attribute, handle namespaces explicitly, and assert on the value that actually changes the response. This is the reliable way to stub legacy SOAP services where the same envelope and operation carry different payloads.

Why whole-body matching fails for XML

XML is verbose and sensitive to detail that does not change meaning. A namespace prefix can be declared differently, whitespace can shift, or an element order can vary between clients, and an exact string comparison will treat those as different requests even though the logical call is the same. XPath solves the problem by reading the document as a tree and matching on the value of a particular node rather than the serialized text, so irrelevant formatting differences stop breaking the stub.

Matching styleWhat it comparesWeakness
Exact body stringSerialized textBreaks on whitespace or prefixes
XPath node valueA specific elementRequires namespace handling
XPath attributeA named attributeMust be present in the schema
Combined matchersSeveral nodes togetherMore expressive, slightly more setup

A SOAP body that needs distinguishing

Suppose two requests share the same envelope and operation, but the account type differs inside the body. That single element is what should decide which response the mock returns.

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
               xmlns:acc="http://example.com/accounts">
  <soap:Body>
    <acc:GetAccount>
      <acc:AccountType>business</acc:AccountType>
      <acc:AccountId>1001</acc:AccountId>
    </acc:GetAccount>
  </soap:Body>
</soap:Envelope>

The distinguishing value is the AccountType node, so the matcher should read that node rather than compare the entire envelope. A second request with personal in the same node can then be routed to a different stub while everything else stays identical.

Defining the XPath matcher

WireMock supports XPath matching, and the expression namespaces the node you want. Because SOAP uses declared namespaces, the expression must account for them rather than treating the element as a bare name. The matcher can combine the node check with an equality condition so the stub only fires when the element holds the expected value.

{
  "request": {
    "method": "POST",
    "urlPath": "/soap/accounts",
    "bodyPatterns": [
      {
        "matchesXPath": "//acc:GetAccount/acc:AccountType[text()='business']",
        "xpathNamespaces": {
          "acc": "http://example.com/accounts",
          "soap": "http://schemas.xmlsoap.org/soap/envelope/"
        }
      }
    ]
  },
  "response": {
    "status": 200,
    "bodyFileName": "business-account.xml",
    "headers": { "Content-Type": "text/xml" }
  }
}

Namespace and prefix pitfalls

The most common XPath failure is ignoring namespaces. A document can use any prefix, including a default namespace, so an expression that assumes a bare element name will not match. Declare the namespace map and use the logical names, and test the expression against a real sample rather than assuming the path. Attribute matching is another useful variant: when the payload distinguishes records by an attribute such as a status code, read that attribute directly instead of adding a second element check.

Combining matchers for precision

One node is often enough, but a complex operation may need several conditions to avoid an accidental match. Combine the XPath for the operation with a second matcher for the ID or type, so a stub cannot fire on a different call that happens to share the same element value. The tradeoff is modest: more expressions mean slightly more setup, but they keep the stub deterministic as the number of legacy operations grows.

How an agent mishandles it

An agent that is asked to stub SOAP often starts with an exact body copy, which passes for the first request and breaks as soon as the client changes a prefix or adds whitespace. Ask it to identify the distinguishing node and express that as an XPath matcher with the namespace map. If the agent writes a path that ignores the namespace and then reports the mock as set up, the failure will only appear at runtime; require a test request against the stub to confirm the match.

Verifying the matcher against real samples

XPath expressions are easy to get subtly wrong, so always verify against a captured sample rather than assuming the path resolves. Send the representative request through the stub and confirm it selects the intended response, then send the near-identical request with a different node value and confirm it does not match. This pair proves the matcher is reading the distinguishing element and not firing too broadly on a shared envelope.

# Evaluate the XPath against a sample before wiring it into the stub
xmllint --xpath "//acc:GetAccount/acc:AccountType/text()" sample.xml
# business

# The near-identical request should return the other stub
xmllint --xpath "//acc:GetAccount/acc:AccountType/text()" sample-personal.xml
# personal

Keeping the sample files in the repository alongside the stub gives future changes a reliable reference and makes a broken namespace path fail in review rather than in the middle of a legacy integration. Once the expressions are verified, the mock can distinguish the two SOAP calls deterministically despite their near-identical bodies.

Frequently asked questions

Why not just match the full SOAP body?

Because XML formatting, prefixes, and whitespace can change without altering the meaning, so an exact string comparison treats equivalent calls as different. XPath reads the logical tree and targets the element that matters.

How do I handle the SOAP namespaces?

Declare the namespace map in the matcher and reference the elements by their logical names, since the prefix in the document can differ from the one in your expression.

Can I match on an attribute instead of an element?

Yes. XPath can read a named attribute, which is useful when the payload distinguishes records by an attribute rather than a child element.

When should I combine multiple XPath matchers?

When several operations share the same node value, add conditions for the operation and the ID together so a stub cannot fire on the wrong call.

This is part of the Powerduck vs WireMock: Spec-First vs Code-First Mocking — start there for the full comparison of mocking approaches.