skip to content

Why do literal values over-specify a Pact consumer expectation, and what does a type matcher change?

level: middleimportance: should knowfreq 68%

answer

  1. What happens where no rule applies?
  2. Timestamps and generated ids never repeat
  3. The example becomes illustration, not assertion
  4. Ask what your code does with the value
  5. Presence is still required either way

basics

~20 s

Where no matching rule covers a path, Pact compares by equality, so every literal you record becomes a value the provider must return forever. A type matcher replaces that with 'present, and of this JSON type' at that path.

solid answer

~40 s

Pact's default comparison is equality, so a pasted-in id, timestamp or price is an obligation the provider cannot meet — verification goes red on data rather than on breakage, and the team stops trusting the job. A **type matcher** (`like` in pact-js, `stringType` or `integerType` in Pact-JVM) relaxes one path to "present, same JSON type"; a **regex matcher** relaxes it to "present, string, matching this pattern". The rule is written into the pact file, so it is the strictness the provider is checked against, not consumer-side sugar. Choose by use: type-match what you pass through, value- or regex-match what your code interprets. Relax further and you stop seeing vocabulary and format changes — and note a matcher never makes a field optional.

code

javascript · 17 lines
javascript
const { PactV3, MatchersV3 } = require('@pact-foundation/pact');
const { like, regex, integer, eachLike } = MatchersV3;

pact
  .given('rental 84213 is active')
  .uponReceiving('a request for rental 84213')
  .withRequest({ method: 'GET', path: '/rentals/84213' })
  .willRespondWith({
    status: 200,
    headers: { 'Content-Type': 'application/json' },
    body: {
      rentalId: integer(84213),          // any integer
      status: regex('ACTIVE|RETURNED', 'ACTIVE'),  // vocabulary we branch on
      monthlyPriceCents: integer(4995),  // any integer
      instruments: eachLike({ sku: like('cello-half-size') })
    }
  });

go deeper

for a junior

Recall that a Pact expectation can assert either an exact value or just a shape, and that hard-coding generated values such as ids and timestamps is what makes contract tests fail for reasons unrelated to the API.

for a middle

Explain the mechanics: equality is the default where no rule covers a path, a type matcher asserts presence and JSON type, a regex matcher asserts a pattern, and every one of those choices is written into the pact file and enforced against the provider.

for a senior

Demonstrate matcher discipline as a judgement call — type for pass-through values, value or regex for anything your code interprets — and name what relaxation hides: enum drift and unit changes that keep the same JSON type.

for a principal

Own the failure mode at organisation scale: over-specified contracts make verification flaky, flaky verification gets muted, and muted verification is worse than none. Set the review convention for what may be asserted literally.

## Equality is the default, and it is expensive Pact compares a recorded expectation with the provider's actual response path by path. Where no matching rule covers a path, the comparison is **equality**: the literal example you typed in the consumer test becomes a value the provider is obliged to return on every verification run, forever. That is fine for a handful of fields and ruinous for the rest, because real responses are full of values no provider can reproduce on demand: - surrogate ids drawn from a sequence - `createdAt` / `updatedAt` timestamps, which are "now" by definition - generated tokens, ETags and signed URLs - prices, counts and balances that move with the data - array ordering that the provider never promised to keep stable Each of those produces a red verification build caused by **data**, not by a contract change. Teams that hit enough of those stop trusting the signal, mark the verification job non-blocking, or delete the pact — which is why over-specification, not under-specification, is the usual way a Pact rollout dies. ## What each matcher relaxes the assertion to A matcher replaces equality at one JSON path with a weaker predicate. The example value stays in the file — it is what the mock provider serves during the consumer test and what a reader sees — but it is no longer the assertion. | Matcher | What the provider must satisfy | Fits | |---|---|---| | type (`like` in pact-js; `stringType`, `integerType`, `booleanType` in Pact-JVM) | field present, same JSON type, value irrelevant | ids, names, free text you pass through | | regex (`regex` in pact-js; `stringMatcher` in Pact-JVM) | present, a string, matches the pattern | codes, enums, formats you parse | | numeric flavours (`integer`, `decimal`, `numberType`) | present and numeric of that flavour | quantities, money in minor units | | collection templates (`eachLike`, `minArrayLike`) | every element matches the element template | lists whose element shape you read | | none | equal to the recorded example | the small set of values your code branches on | Two things a matcher does **not** do. It does not make the field optional — a type matcher still requires the field to be present, so a provider that drops it fails verification. And it is not consumer-side sugar: the rule is written into the pact file and is what the provider is checked against, so the choice you make in the test is the strictness the provider lives with. ## Where the line sits The workable rule is a question about your own code: **does anything in the consumer interpret this value, or does it only pass through?** - Pass-through — rendered, stored, forwarded, logged: match on **type**. You depend on presence and shape, nothing more. - Interpreted — parsed, compared, switched on, used to pick a branch or a downstream call: match on the **format or the value**. A `status` your client switches on deserves a regex naming the vocabulary you handle; a date you parse deserves a matcher for the format you parse. - Structural — the field must simply exist for deserialisation to succeed: type matcher, and resist the urge to add more. ## What you stop detecting when you relax too far Relaxation is not free, and the losses are quiet: 1. **Vocabulary drift.** A type matcher on a status accepts `ACTIVE`, `active` and `WITHDRAWN` alike. If your client has a branch per value, the provider can add or rename one and the pact stays green while production takes an unexpected path. 2. **Semantic change under a stable type.** A price moving from whole units to minor units is an integer before and after. Type matching cannot see it; nothing in Pact can. That is a genuine limit of the technique, not a configuration mistake. 3. **Format change inside a string.** A date rendered a new way, an identifier that gains a prefix, a currency symbol appearing in an amount — all still strings. 4. **Over-broad patterns.** A regex of `.*` is a type matcher wearing a costume, and reads to the next maintainer as though a real constraint exists. 5. **Deletion as noise control.** Removing a field from the expectation because it kept failing does not loosen the assertion; it removes the obligation entirely, and the provider is then free to stop sending a field your client reads. The healthy end state is a contract where every remaining literal is deliberate and every matcher is justified by what the consumer does with the value — small, stable under provider change, and still capable of failing for a reason that matters.

  • When is an exact literal the right choice in a Pact consumer expectation?
    When the consumer interprets the value rather than passing it through: a status your client switches on, an error code that selects a retry path, a currency your conversion assumes. There the value itself is the thing you depend on, so equality — or a regex naming the vocabulary you handle — is the assertion you actually want.
  • Does putting a type matcher on a field make it optional in the contract?
    No, and this trips people up. A type matcher relaxes the value comparison, not the presence requirement: the field must still appear in the provider's response with a value of that type, or verification fails. To let the provider stop sending a field, remove it from the expectation entirely — which also removes the obligation.
  • Your regex matcher for a status field is `.*`. What is wrong with that?
    It is a type matcher in disguise — every string satisfies it — but it reads to the next maintainer as though a real constraint exists. Either name the values you actually handle so a vocabulary change fails the build, or use a type matcher honestly so nobody believes the field is checked.

A literal expectation is a photograph of one response; a matcher is a description of it. You want a description of the parts you depend on, not a photograph of the whole scene.

saying these in an interview costs you the question

  • Records exact values everywhere to be 'strict'
  • Type-matches a status field the client branches on
  • Thinks a matcher makes the field optional
  • Believes matchers only affect the consumer-side test
  • Keeps a literal timestamp and blames the provider for flakiness
  • Deletes a noisy field instead of loosening its matcher