Mariner/ docs← marinerhttp.dev

02 · GUIDES

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):

  • 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 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, across 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.

View as Markdown ↗