# Contracts

> Derive a JSON Schema from a response that already works, and every response after it gets measured against that shape — in the response pane, in the collection runner and over MCP.

## Capture it, don't write it

Nobody writes a JSON Schema by hand, which is why contract testing usually never starts. Mariner runs the gesture the other way round:

1. Send the request while the API is behaving.
2. Open the **Contract** tab of the request editor — between Variables and Post-Response — and press **Derive from response**.
3. That's the contract. Every response from now on is measured against it.

The tab carries a `•` whenever the request promises a shape, so you can tell before spending a send. It opens on **Shape**: one row per field with its name, a type chip, a `REQ` mark when the schema demands it, and whatever else it demands — `format: date`, an enum's members, a range. Twenty lines of braces become eight rows you read in two seconds. **JSON** is one segment away with the document itself, and it warns the moment what you type stops being valid JSON.

**Re-derive** replaces the contract after the API legitimately changes; **Clear** drops it. Without a subscription the header shows a *Read-only* chip instead: the schema stays on screen to read, and editing it waits until you subscribe.

The schema is a JSON Schema (draft 2020-12) document, titled after the request, and it stays editable underneath — paste one translated from your Zod, Pydantic or OpenAPI definition and it works just the same.

The welcome collection ships with one already wired up: **Contract → GET · A response under contract**. Send it and the verdict appears; change `/json` to `/uuid` in the URL and watch it break.

## What "derived" means

Deriving reads the response you have, not the API you wish you had:

- **Only what was actually there is required.** For an array, a key present in every sampled element is required; a key that appears in some of them is described but optional.
- **Extra fields never break it.** `additionalProperties` is not emitted, so an API that adds a field keeps passing — the contract catches things that *disappeared* or *changed type*, which is what actually breaks a client.
- **A field that is sometimes null** comes out as `["string", "null"]` rather than as a lie.
- **A field whose samples disagreed** comes out as an empty schema — anything validates there. Guessing would reject payloads the API really sends.
- Arrays are sampled to 200 elements and nesting to 8 levels.

## The verdict, where you are already looking

- A **CONTRACT** chip sits next to the status: green when the response matches, amber with a count when fields drifted, red when the schema itself isn't valid JSON. Click it to jump to the tab.
- The response pane's **Contract** tab carries an amber badge with the number of violations — lit even when you are on another tab, because its job is to tell you from the tab you are not looking at.
- That tab is a table, not a list of complaints: every field the contract describes, with **the value that actually arrived** next to it, and the drifted ones sorted to the top with the reason on their own line. **Only differences** hides the agreeing majority, which is what makes a forty-field contract readable. Right-click a row to copy its path.
- Each violation is listed with its path (`$.slideshow.author`) and what went wrong: *Expected string, got integer*, *Required field `id` is missing*, and so on. The list stops at 100.
- No contract, or a response that isn't JSON? Nothing is shown. A permanent "no contract" chip over the 90% of requests that will never have one is exactly what teaches people to stop reading the strip.

**A broken contract never fails the request.** The server answered; the status is what it is. Validation is advisory and says so — it reports which field moved, and leaves the verdict on the send to HTTP.

## What the validator covers

Structure (`type`, `properties`, `required`, `items`, `additionalProperties`), values (`enum`, `const`, `minimum` / `maximum`, `minLength` / `maxLength`, `pattern`, `minItems` / `maxItems`, `uniqueItems`, `multipleOf`) and composition (`anyOf`, `allOf`, `oneOf`, `not`, plus local `$ref` into `$defs`).

Not implemented, on purpose: `$dynamicRef`, `unevaluatedProperties` and remote `$ref` resolution — they show up in spec test suites, not in a schema derived from your own endpoint. `format` is treated as the annotation the spec says it is: a `"date-time"` that doesn't parse is not a failure, because half the APIs in the world would fail their own contract on that alone.

## In the collection runner

Run a collection and every request that carries a contract is checked as it goes ([Runner](/docs/runner)):

- The summary adds **Off contract** and **No contract** next to Passed / Failed — but only when at least one request in the run has a contract. A zero-drift count over nine requests nobody checks is not good news, so the two numbers always travel together.
- Each row gets a contract cell: a green seal, an amber count, a red mark for an unusable schema, or `—` when there is nothing to check.
- The exported report carries it too. The CSV has a `contract` column and the JSON a `contract` field, both with `passed`, `failed(n)`, `invalid_schema` or `not_json` — the same words an agent gets over MCP, so a CI job and an agent can talk about the same run without translating.

## From an agent

The [MCP server](/docs/mcp) exposes the same verdict, computed by the same code:

| Tool | Purpose |
|---|---|
| `get_contract` | Read the schema document a request promises |
| `set_contract` | Store a schema you built, or pass `derive_from_response: true` to send once and derive it. Returns the previous schema |
| `check_contract` | Send and check. `variants: "all"` runs every saved body variant in turn and restores the one that was active |
| `check_collection_contracts` | "Did this API change?" for a whole collection; requests without a contract are reported as unchecked, never silently skipped |

`send_request` and `run_collection` include the verdict as well, so an agent that was doing something else still learns the shape moved.

## Where the schema lives

The contract belongs to the request, not to a response: it is part of the definition, so it travels with the request through [export and import](/docs/file-format), across [iCloud sync](/docs/system#icloud-sync), and is what `get_contract` reads over MCP. Hand a `.mariner` package to a colleague and the contracts arrive with it. It is stored as the raw document — a schema you pasted from your own repo comes back out byte-identical instead of reformatted.

A request with no contract writes no key at all, so a package from someone who never touched the feature keeps exactly the shape it had.

<aside class="callout" data-label="THE FREE MONTH">
Contracts are part of every plan, and of the <a href="/docs/free-and-pro">free month</a>. The welcome collection ships with one already derived, so the verdict is something you can see working on day one.
</aside>

---
Source: https://marinerhttp.dev/docs/contracts · Mariner docs
