A response can be valid JSON and still violate an API contract. An identifier may arrive as a string instead of an integer, a timestamp may lose its timezone, or a required nested object may disappear. Pydantic models give tests a compact way to parse and validate those structures. The important design decision is how much conversion to allow. Convenient coercion helps application code accept varied inputs; contract tests often need a sharper signal when the wire format changes.
Separate JSON syntax from domain shape
Calling response.json() proves that the body can be decoded as JSON. It does not prove that required fields exist, types are correct, or values satisfy constraints. A model adds that second layer. It can express nested structures, constrained numbers, enums, timestamps, and optional fields in a form that both tests and reviewers can understand.
The model should describe the public boundary, not mirror an internal database entity. Internal columns, ORM relationships, and service-only flags make a contract model brittle and may accidentally bless data that should never leave the service. Name the model after the representation or operation—OrderResponse or CreateOrderRequest—so request and response rules do not drift into one ambiguous object.
Understand coercion before enabling strictness
In its normal mode, Pydantic converts many compatible inputs. For example, a numeric string may become an integer. That is useful when reading forms or environment variables. In a response contract test, the same conversion can hide a producer that changed 42 to "42". Strict validation rejects conversions that the selected type does not permit in strict mode, making representation changes visible.
Strictness can be applied per validation call, per field, or through model configuration. Those levels support mixed contracts. An identifier can be strict while a legacy field remains coercible during a migration. There are also differences between validating Python objects and validating JSON input for some standard types, so a suite should choose model_validate or model_validate_json intentionally and test the actual wire representation it cares about.
from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field
class OrderResponse(BaseModel):
model_config = ConfigDict(strict=True, extra="forbid")
id: int
status: str
total_cents: int = Field(ge=0)
created_at: datetime
payload = response.json()
order = OrderResponse.model_validate(payload)
assert order.status == "confirmed"Choose an extra-field policy by compatibility goal
Forbidding extra fields is useful when the exact representation is controlled and an unexpected field may expose sensitive data. It also catches undocumented additions. The tradeoff is forward compatibility: many APIs consider adding an optional response field non-breaking. A consumer-focused test may safely ignore additional fields while still requiring known fields and types.
There is no universal setting. Provider contract tests may forbid extras to keep documentation and implementation aligned. A resilient client test may allow them because the client should continue working after an additive change. State that policy in the model configuration rather than relying on a default that nobody reviewed. For security-sensitive objects, add explicit assertions that secrets, internal tokens, or personal fields are absent even if general extras are allowed.
Keep validation layers distinct
Field constraints protect local facts: an amount is non-negative, a string has a maximum length, or a value belongs to an enum. Model validators are appropriate for relationships within one representation, such as an end time occurring after a start time. Rules that depend on database state, permissions, or another service are business behavior and should usually remain in scenario assertions.
Mixing all three layers into a model creates confusing failures. A schema error should say that the representation is malformed. A business assertion should say that the state transition is wrong. That distinction makes reports actionable: the first points toward serialization or documentation, while the second points toward application logic. It also prevents a contract model from making network calls or acquiring hidden test dependencies during validation.
Model stable guarantees, not incidental payloads
Copying a large sample response into a generated model can preserve accidental details. Start from the published schema and the fields that clients rely on. Use enums only when the value set is genuinely closed; otherwise, a newly added status can break a consumer even if the API intended it as an extension. For timestamps, validate timezone expectations explicitly if ordering across systems depends on them.
Pydantic can generate JSON Schema from models, which is useful for review and comparison, but avoid creating two ungoverned sources of truth. If OpenAPI is authoritative, models should be checked against it or generated through a controlled process. If code-first models are authoritative, publish their schema consistently. Automation is most trustworthy when a contract change produces one clear review point rather than silent drift between documents and tests.
Practical takeaways
What to carry into the next test suite
- Validate decoded JSON against an explicit boundary model, not an internal persistence model.
- Use strict mode where wire-type coercion would hide a breaking change.
- Choose the extra-field policy according to provider control, client compatibility, and security risk.
- Keep representation validation separate from stateful business assertions.
References
Primary documentation and technical references used in this article.