skip to content

In a generated Pact file, what does one interaction contain and where do its matchingRules live?

level: middleimportance: nice to knowfreq 26%

answer

  1. Four keys at the top of the file
  2. Rules travel with the message they constrain
  3. Categories first, then a path expression
  4. An uncovered leaf is a literal requirement
  5. Check the specification version in metadata

basics

~20 s

An interaction holds a description, provider states, a request block and a response block. Matching rules live inside that interaction's own request and response objects, keyed by category such as body or header and then by JSON path.

solid answer

~40 s

A pact file has `consumer`, `provider`, `interactions` and `metadata` at the top level. Each interaction carries a `description`, a `providerStates` array naming preconditions, a `request` (method, path, optional query, headers and body) and a `response` (status, optional headers and body). `matchingRules` is **inside** the request and response objects — never at the top of the file — structured as category (`body`, `header`, `path`, `query`, `status`) then JSON path then a `matchers` array such as `{ "match": "type" }`. To read the provider's obligation, walk the response body and check each leaf for a covering rule: covered leaves are matched by that rule, uncovered leaves are required to equal the example exactly.

code

json · 27 lines
json
{
  "consumer": { "name": "rental-web" },
  "provider": { "name": "instrument-catalog" },
  "interactions": [
    {
      "description": "a request for rental 84213",
      "providerStates": [ { "name": "rental 84213 is active" } ],
      "request": { "method": "GET", "path": "/rentals/84213" },
      "response": {
        "status": 200,
        "headers": { "Content-Type": "application/json" },
        "body": {
          "rentalId": 84213,
          "status": "ACTIVE",
          "sku": "cello-half-size"
        },
        "matchingRules": {
          "body": {
            "$.rentalId": { "matchers": [ { "match": "integer" } ], "combine": "AND" },
            "$.sku": { "matchers": [ { "match": "type" } ], "combine": "AND" }
          }
        }
      }
    }
  ],
  "metadata": { "pactSpecification": { "version": "3.0.0" } }
}

go deeper

for a junior

Recall that a pact file is plain JSON naming one consumer and one provider, and that its interactions array holds the requests and responses recorded by the consumer test rather than any executable code.

for a middle

Be able to navigate the file: the four top-level keys, the parts of an interaction, and the fact that matching rules sit inside each request and response, organised by category and then by JSON path expression.

for a senior

Show you can read an obligation off the artefact without running anything — walking the response body leaf by leaf, deciding which are matcher-covered and which are literal — and use that when reviewing a diff to a consumer's contract.

for a principal

Treat the file as the durable interface record between teams: what a reviewer should look for in its diff, and what it deliberately does not contain, such as the provider's setup for each named state.

## The shape of the file A pact file is plain JSON with four top-level keys: `consumer` and `provider` (each an object with a `name` — together they identify the pair the file belongs to), `interactions` (the array that carries the content), and `metadata` (which spec version and which library wrote it). The name matters because the file is the unit of exchange: one file records everything one consumer expects from one provider, and provider verification replays every interaction in it. ## Anatomy of one interaction An interaction is an object with roughly these members: - `description` — the human label from `uponReceiving`, and the identity of the interaction. - `providerStates` — an array of `{ name, params }`, naming the precondition the provider must set up before this interaction is replayed. The state name is a string the provider side is expected to recognise; the pact file itself carries no setup code. - `request` — `method`, `path`, and optionally `query`, `headers`, `body`, plus its own `matchingRules` and `generators`. - `response` — `status`, and optionally `headers`, `body`, plus its own `matchingRules` and `generators`. `generators` is worth recognising even though it is rarer: it produces a value at replay time (a fresh id or timestamp) rather than asserting one, which is the mechanism behind values that cannot be fixed in the file. ## Where the matching rules live `matchingRules` is **inside the request object and inside the response object of each interaction** — not at the top of the file, not inline in the body. Its structure is category → path → rules: ``` "matchingRules": { "body": { "$.rentalId": { "matchers": [ { "match": "integer" } ], "combine": "AND" } }, "header": { ... } } ``` Categories are the parts of the message a rule can apply to — `body`, `header`, `path`, `query`, `status`. Under `body`, keys are JSON-path expressions against that body, including wildcards such as `$.instruments[*].sku` for every element of a collection. Each entry holds a `matchers` array — `{ "match": "type" }`, `{ "match": "regex", "regex": "..." }`, `{ "match": "integer" }` and so on — and a `combine` value saying how multiple matchers on one path are combined. The body itself still contains ordinary example values. Those examples are what the consumer's mock provider served during the consumer test, and they are what a human reads to understand the interaction — but wherever a matching rule covers the path, the example is illustration, not assertion. ## Reading the obligation off the file That gives a mechanical procedure for answering "what is the provider now required to do?" without running anything. For each interaction: 1. Read `request.method` and `request.path`, adjusted by any `matchingRules` under `path` or `query`: that is the call the provider must accept. 2. Read `providerStates`: the provider must be able to reach each named state, or verification of that interaction cannot run. 3. Read `response.status`: that status is required — it is compared by equality unless a rule under the `status` category says otherwise. 4. Walk `response.body` leaf by leaf. For each leaf, look for a `matchingRules.body` entry whose path expression covers it. - **Covered** — the obligation is the matcher: same type, or matching the regex, and present. - **Not covered** — the obligation is the literal example. This is the step people skip, and it is where surprise red builds come from: an unremarkable-looking `"status": "ACTIVE"` with no rule beside it means exactly that string, every time. 5. Anything **not** mentioned in the response body is unconstrained — the provider may add, remove or change it freely as far as this consumer is concerned. Reading a pact this way is a genuinely useful review skill. Diffing the file across a change shows whether a consumer edit tightened the contract, and a file where nearly every leaf lacks a matcher is a contract that will fail on data rather than on breakage. ## Reading `metadata` first `metadata.pactSpecification.version` tells you which spec the file follows, which changes what you should expect to see. In V2 an interaction carried a single `providerState` string and matching rules were flat entries keyed with a `$.body.` prefix. V3 replaced that with the `providerStates` array, grouped rules under categories, and added `generators`. V4 additionally labels each interaction with a type, so synchronous HTTP interactions and asynchronous message interactions can live in one file. Check that key before concluding a file is malformed — more often it was written against a different specification version than the one you have in mind.

  • Reading a pact file, how do you tell which response fields the provider may change freely?
    Anything not mentioned in the response body at all — extra fields are ignored during matching, so they are unconstrained by this consumer. For fields that are mentioned, check `matchingRules.body` for a covering path: a type rule leaves the value free but the field required, while no rule at all pins the exact example value.
  • What is the `generators` block for, as distinct from `matchingRules`?
    Matching rules describe how a value is compared; generators produce a value at replay time instead of asserting one, which is how fields that cannot be fixed in a file — a fresh identifier or a current timestamp — get a usable value when an interaction is replayed. Seeing both on one path is normal.

saying these in an interview costs you the question

  • Looks for matchingRules at the top level of the file
  • Reads every body value as an exact requirement
  • Reads every body value as merely an example
  • Thinks the file contains provider setup code
  • Ignores metadata.pactSpecification.version when the shape looks wrong