How to Keep API Contract Tests Stable When Fields Are Optional, PATCH Is Partial, and Versions Drift
By Luca Müller · November 9, 2026
A practical guide to test API contracts with optional fields, handle PATCH semantics, and prevent brittle assertions when schemas evolve.
Optional fields are where otherwise solid API test suites start to rot. A response that was stable yesterday now includes a new nullable property, a PATCH endpoint legitimately returns only the fields it changed, or a versioned service keeps old clients alive while introducing a newer shape. If your tests still compare full JSON bodies byte for byte, they will fail for the wrong reasons.
The fix is not to make assertions weaker everywhere. The useful approach is to separate contract rules from response shape details:
- required fields, which must always exist and usually need type checks,
- optional fields, which may exist but should not break consumers when absent,
- PATCH semantics, where absence can mean “unchanged” rather than “missing,”
- version drift, where compatibility matters more than exact payload identity.
This article shows how to test API contracts with optional fields without making your suite brittle. The core idea is to assert the invariant parts of the contract, then choose the lightest validation that still catches real regressions.
Start by defining what the contract actually guarantees
Before you write tests, decide whether the API promises:
- Presence of a field,
- Type of a field,
- Value constraints on a field,
- Relationship between fields,
- Behavior across versions.
Those are not the same thing.
An OpenAPI document is a good place to make that distinction explicit, because it separates required properties from optional ones and can describe nullability, formats, and payload shapes. The specification is the reference point for this style of contract documentation, not a full implementation guarantee by itself. See the OpenAPI Specification.
A stable contract test suite checks what the API promises, not every property the server happens to return today.
A useful mental model is this:
- Schema validation answers, “Is this response structurally allowed?”
- Field assertions answer, “Did this request produce the business outcome I expected?”
- Consumer-driven contracts answer, “Will this provider still satisfy a known client?”
Use the smallest tool that answers the question you actually have.
Separate required fields from optional fields on purpose
If a field is optional, your test should not fail just because it is absent. That sounds obvious, but many suites still treat “expected JSON” as a complete snapshot.
A better rule is:
- assert required fields are present,
- assert optional fields only when the scenario depends on them,
- if an optional field appears, validate its type and meaning.
Example: response contract with optional fields
Suppose GET /users/123 returns:
{ “id”: “123”, “email”: “ada@example.com”, “displayName”: “Ada”, “phone”: null, “marketingOptIn”: true }
A brittle test would compare the entire object exactly. A better test checks the invariants:
const res = await fetch("https://api.example.com/users/123");
const body = await res.json();
expect(res.status).toBe(200); expect(body).toMatchObject({ id: “123”, email: “ada@example.com” }); expect(typeof body.displayName).toBe(“string”);
if (“phone” in body && body.phone !== null) { expect(typeof body.phone).toBe(“string”); }
This style avoids forcing optional fields into the test unless they are part of the scenario. It also makes schema drift easier to diagnose, because failures point to the missing or wrong field instead of a giant diff.
A simple rule for optional fields
If a field is optional because the server may omit it, do not encode it as required in tests.
If a field is optional but important when present, add a conditional assertion for type or format.
If a field is optional only in older versions, write version-specific tests rather than one universal snapshot.
Treat PATCH differently from POST and PUT
Partial update APIs are a separate class of problem. PATCH often means the client sends only the fields to change, and the server returns either the full updated resource or only a partial representation. Both are valid patterns, but they require different assertions.
The biggest mistake is assuming that a PATCH response must echo the full resource. That may be true for one service and wrong for another.
Three questions to answer first
- Does the request body allow partial fields only?
- Does the response return the full resource, a minimal acknowledgment, or the patched fields only?
- Does an omitted field mean “unchanged,” or can omission itself be an error?
A correct PATCH test usually checks the precondition, the changed fields, and one or two untouched fields that prove the server did not overwrite data accidentally.
Example: partial update test
const patchRes = await fetch("https://api.example.com/users/123", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ displayName: "Grace" })
});
expect(patchRes.status).toBe(200);
const patched = await patchRes.json();
expect(patched.displayName).toBe(“Grace”); expect(patched.email).toBeDefined(); // if the API promises a full resource
If the endpoint returns only changed fields, this last assertion should not exist. Instead, test the follow-up GET:
const getRes = await fetch("https://api.example.com/users/123");
const user = await getRes.json();
expect(user.displayName).toBe("Grace");
expect(user.email).toBe("ada@example.com");
That separation matters because PATCH failures often hide in two places:
- the patch handler accepts the request but does not persist the change,
- the response looks correct, but the stored resource is wrong.
Avoid full-body assertions unless the payload is intentionally fixed
Full-body equality is tempting because it is easy to write. It is also the fastest way to make your suite fail on harmless additions such as a new optional field, metadata, or server-generated timestamp.
Use full-body assertions only when all of these are true:
- the endpoint is meant to return a fixed, small, fully specified payload,
- the contract is stable and unlikely to evolve frequently,
- there is no versioning pressure,
- every field matters to the consumer.
For fast-moving APIs, prefer one of these patterns instead:
- subset matching for required fields,
- type and format checks for optional fields,
- schema validation for structural rules,
- business assertions for scenario-specific outcomes.
Practical decision rule
If the test fails because a harmless field was added, your assertion is too strict.
If the test passes while a required field is missing or malformed, your assertion is too weak.
The goal is not maximum strictness. The goal is the narrowest assertion that still protects the client.
Use schema checks for structure, not as the only test
JSON Schema and OpenAPI-based validation are useful because they capture allowed shapes, types, enumerations, and required properties. They are especially helpful when response schema drift is the main risk.
But schema validation alone has a blind spot, it cannot tell you whether the API returned the right business data.
A good split looks like this:
- schema validation catches shape regressions,
- targeted assertions catch functional regressions.
Example: schema-first validation mindset
If a response allows either phone to be absent or to be a string, encode that in the schema and test the behavior separately. A schema can say “optional and string when present,” but it cannot tell you whether the phone number should appear for a verified user.
That distinction is important when teams confuse documentation with behavior. A spec can describe a field as optional, but only a test can verify the scenario under which it should appear.
Handle version drift with compatibility rules, not snapshots
Version drift is usually a compatibility problem, not a data problem. New versions may add fields, deprecate fields, change defaults, or widen response shapes. The testing strategy should reflect that.
Backward-compatible API changes you can usually allow
- adding optional fields,
- adding new enum values, if consumers are designed to ignore unknown values,
- adding metadata fields,
- returning extra fields in responses when clients ignore them safely.
Changes that deserve a failing contract test
- removing a required field,
- changing a type, for example number to string,
- changing a field from object to array,
- reusing a field name with a different meaning,
- changing PATCH semantics so that omission no longer means “unchanged.”
The question is not “Did the payload change?” The question is “Did the payload change in a way that breaks an existing client?”
A versioned suite works best when each consumer or major client has its own contract expectations. That can be a consumer-driven contract, a version-specific schema, or a compatibility test matrix. The exact form matters less than the rule: do not mix incompatible expectations into one generic assertion.
A practical test matrix for flexible APIs
You do not need one test per field. You need a matrix that covers behavior classes.
| Scenario | What to assert | What not to assert |
|---|---|---|
| Required field response | Presence, type, and business value | Exact order of unrelated fields |
| Optional field absent | Response still valid, required fields intact | That the field must always exist |
| Optional field present | Type, format, and meaning | Exact payload snapshot |
| PATCH request | Changed fields persisted, unchanged fields preserved | Full response body equality if the API does not guarantee it |
| New API version | Added fields do not break clients | Old snapshot remains byte identical |
This table is intentionally small. If you cannot classify a test into one of these rows, the contract is probably underspecified.
When consumer-driven contracts are better than hand-written checks
Consumer-driven contracts are the right move when multiple clients depend on the same provider and each client has different tolerances for optional fields or version changes. The point is to validate the provider against the actual consumer expectations, not an abstract “perfect” schema.
Use them when:
- the provider serves multiple frontends or services,
- payloads differ by client capability,
- version drift is a recurring release risk,
- you need a clear compatibility gate in CI.
Hand-written tests are still fine when:
- one team owns both client and provider,
- the API surface is small,
- the main risk is business logic rather than compatibility,
- you need fast, readable checks around a few critical endpoints.
A maintenance checklist for stable contract tests
If your API suite is already brittle, do this in order:
- Inventory tests that compare full JSON bodies.
- Mark each field as required, optional, deprecated, or scenario-specific.
- Rewrite assertions to check only required fields globally.
- Add conditional checks for optional fields only in relevant scenarios.
- Separate PATCH tests into request validation, persistence checks, and response-shape checks.
- Add version-specific expectations where backward compatibility differs.
- Keep one source of truth for response schemas or contract definitions.
A quick review pass often reveals that many “contract failures” are actually assertion failures. The API did something acceptable, the test simply expected too much.
Who should skip a schema-heavy approach
A schema-heavy approach is not the best fit if your API is intentionally tiny, changes infrequently, and has a single tightly controlled consumer. In that case, a few readable field assertions may be enough.
It is also a poor fit if the team cannot keep schema documents current. An outdated schema creates false confidence, which is worse than a noisy test.
For teams with many clients, frequent release cadence, or lots of optional fields, schema-backed contract tests are usually worth the extra discipline.
Bottom line
To test API contracts with optional fields, do not treat every response as a fixed snapshot. Define the required fields, allow optional fields to come and go without breaking unrelated tests, and write PATCH checks around persistence rather than full-body equality. Use schema validation to catch structural drift, field-level assertions to prove business behavior, and consumer-driven contracts when compatibility depends on the client.
The stable pattern is simple: assert the guarantee, not the accident of one payload.
FAQ
Should optional fields be included in contract tests?
Yes, but only when their presence matters to the scenario. Otherwise, check that the response remains valid without them.
Is JSON Schema enough for API contract testing?
No. It is good for structural validation, but you still need field-level or scenario-level assertions to verify business behavior.
How should PATCH responses be tested?
Test the changed fields, confirm unchanged fields stay intact, and verify persistence with a follow-up GET if the PATCH response does not return the full resource.
What is the safest way to handle schema drift?
Classify fields as required, optional, or deprecated, then make tests strict only on the parts that must stay stable for consumers.
When should I use consumer-driven contracts instead of regular API tests?
Use them when different consumers rely on the same provider and compatibility failures are more expensive than maintaining the contract definitions.