← Writing

engineering

Documentation Is the Contract Surface

A request can match the schema and still do the wrong thing. The docs have to explain what those fields mean in use.

A docs page makes a promise. If it says POST /orders accepts a field, a developer may build around it. If it omits an authentication requirement, someone will discover that omission in production. A tool retrieving the same page can repeat the mistake faster.

That makes documentation part of the interface, even when the API schema is the formal contract.

The gaps are usually small

Consider a request example that still uses a renamed parameter. The endpoint exists, the page loads, and the example looks plausible. A reader copies it and gets a 400. A retrieval system may lift the same example into an answer. Neither can tell from the page that the code changed last week.

We look for three kinds of drift:

  • The reference disagrees with the current schema or source code.
  • The example is syntactically valid but no longer works against the documented version.
  • A guide describes a successful path and skips a condition that changes the result.

A new theme will not fix any of those. A useful review compares the page with the release it describes and runs the examples that can be run.

Small words change the implementation

Take an optional field in an update request. Can you leave it out? Can you send null? Does either action clear the existing value? Those are three different questions, and “optional” only gets you part of the way there.

In OpenAPI 3.1, a schema’s required list determines which properties must be present. Whether a value can be null is a separate schema constraint. The meaning of omitting a field in your update API still needs to be explained and tested. The OpenAPI 3.1 specification defines the schema machinery; it cannot infer your product’s update rules.

Retries have the same problem. Imagine a create request times out after the server accepts it. Telling the reader to “retry on failure” leaves them guessing whether a second request creates a duplicate. If the API supports an idempotency key, document where it goes, how long it is remembered, and what happens when the same key is sent with different input. Only promise the behaviour the service actually implements.

Likewise, a successful submission may mean a job was accepted for later work. The docs need to say how to read its status, which states are final, and where failure appears. An example that prints “Done” immediately after submission teaches the wrong behaviour even if every line runs.

Three valid requests, three different intentions

Consider a hypothetical profile API with an existing displayName of Kam. Its update rules might be:

Request body Intended result
{} Keep the existing name.
{"displayName": "Kam Low"} Replace the name.
{"displayName": null} Clear the name.

All three bodies can pass the same OpenAPI 3.1 schema when displayName permits strings and null, and is absent from the required list. Validation alone cannot tell the caller what happened to the stored value.

This is also where an innocent-looking SDK can change the operation. A helper that removes null-valued properties before sending the request turns “clear the name” into “keep the name”. The request still succeeds. Test the stored result after each case, and show the distinction in the example people will copy.

A password-reset email is a poor place to improvise with a URL. The link may carry a token the application expects to receive intact. Nitrosend’s sending guide makes the behaviour explicit: transactional links are never rewritten for click tracking.

That rule belongs beside the transactional example. A developer looking at a general tracking setting needs to know how it applies to the message they are sending. The request shape cannot supply that explanation on its own.

Timing adds another part of the contract. The same guide explains how tracking changes apply to future rendering and an active campaign’s next batch, and distinguishes existing delivered links from new event collection. A reader asking about a message already in an inbox is asking a different question from someone preparing the next send.

These are three different moments in the same message’s life:

Point in the workflow The documentation should answer
Before rendering Which settings apply to the message being prepared?
During a campaign When does a changed setting begin affecting subsequent work?
After delivery What happens to the links and previously collected events?

Put the answer at the point where someone changes the setting. A developer should not have to infer the behaviour of an already-delivered message from an example that only shows how to send the next one.

A sequence gives the fields meaning

A pagination schema can name every field correctly and still leave the reader to work out the sequence. The Cheesestore reference provides a compact example: the fictional API returns nextCursor, and the next request sends that value as cursor. The default page limit is 20; the maximum is 100.

Show the requests in order. Explain where the cursor comes from, which value to carry forward, and how the API signals that there are no more pages. Keep the version and the demo’s fictional status beside the example so the reader knows what it illustrates.

The reference owns the precise fields. The guide shows how to use them together. Good links let the reader move between those views without maintaining a second, disconnected description of the same contract.

Publish the version a reader can act on

Stable URLs matter because an issue, a search result, or a tool response may point to one exact operation. The page should say which release it describes and link to the related schema and guide. If a claim depends on a dated outside source, show that source and review the claim when it changes.

Sourcey brings the reference and the explanation into one build, from inputs such as OpenAPI, Go comments, Doxygen XML, and Markdown. A field change can reach the generated reference from its source, while the guide beside it explains what the change means. Review the example against the same release so those two parts continue to agree.

A useful docs review can travel with the code change:

  • Change the schema or source comment where the contract is defined.
  • Update the guide that explains the behaviour, including the failure path that changed.
  • Run the example against the version being documented. Check its result, not just its exit code.
  • Open the generated page at its direct URL. Check the parameter, example, and linked schema together.

Where an example can be tested automatically, keep its source with the test. Copying a corrected snippet into three pages just creates three chances to miss the next correction.

The downloaded contract has to be the contract

A renderer needs a convenient internal model: operations, fields, navigation groups and examples. That model serves the page. An API client downloading your OpenAPI definition needs the source contract, including the version, references and extensions it was written with.

Reconstructing a definition from whatever the renderer happened to retain can silently change that promise. A reference may still look right after a constraint or extension disappears from the download. Keep the original definition in the build, resolve its supported local references, and publish a bundle that a caller can actually use away from the source repository.

In Sourcey, contract downloads are an explicit export setting. That distinction also matters when a source file contains operations hidden from the page. Hiding a heading is a presentation choice; distributing the source definition is a publication choice. The two need separate controls.

Then test the files together. Pick a required parameter and a constrained field. Compare the source, the downloaded definition, the browser reference and the operation’s Markdown. Follow the schema link from the exported document. If a caller has to infer the missing relationship from a nearby screenshot, the export has lost part of the interface.

A behaviour change can leave the schema untouched

Suppose an optional parameter keeps the same name and type, but its default changes. Existing callers that omit it may get a different result even though their requests still validate. The schema diff alone may not tell them what to do.

The release note needs to name the affected callers. The reference needs the new default. The guide needs to show the behaviour for the version it documents. If an example depends on the old default, it needs an explicit value or an update.

This is why source ownership matters. The schema should remain the source for the field’s structure. Tested examples should supply the code shown in the docs where possible. Guides should explain decisions and sequences, linking to the reference instead of reproducing whole tables that will drift on the next release.

For an agent consuming those docs, keep the changed default and the affected version close together. For a person reading them, make the migration instruction easy to find. Both need to understand whether their existing request will still do what they intended.

Make the promise easy to find

Good presentation helps here. A required field should be easy to distinguish from an optional one. A warning should sit near the action it affects. The reader should be able to move from an operation to its schema without losing their place.

This is why we spend time on Sourcey’s layout as well as its inputs. A technically complete page can still be miserable to use. Put the actual reference beside the guide, give both some breathing room, and make the links between them obvious.

Open an operation in the Cheesestore OpenAPI demo, follow its schema link, and compare the rendered fields with the source spec. Then follow the pagination example through a second request. What survives when an agent retrieves only part of that page? That is a separate test of the same documentation.