Skip to content

Customizing the generated documentation

Docuccino documents your API by reading your code, and most of the time that’s all you need. When a detail comes out wrong, thin, or in the wrong place, you have three ways to correct it. The trick is picking the right one — the closer a correction lives to the code, the more it survives change.

Start from two questions: is the detail wrong or just missing, and do you own the code it comes from?

The detail is… You own the code? Reach for
A fact about one operation that inference got wrong, or left thinner than you’d like Yes An attribute or docblock at the source
On a route you can’t edit — a vendor or third-party package route — or a batch of spec-side tweaks you’d rather keep out of the code No An OpenAPI Overlay
A rule that should hold across a whole document — naming, wrapping, a synthesized section Either Config, a representation policy, or a DocumentTransformer

The rest of this guide is about the middle row — Overlays — with a note at the end on when to step up to a document-wide transformer.

An OpenAPI Overlay 1.0 document is a small, standard YAML file that lists targeted edits to apply to a specification. Docuccino applies your overlays as part of every build, so the corrections survive regeneration without ever touching the generated document by hand.

resources/docs/overlays/invoicing.yaml
overlay: 1.0.0
info:
title: Invoicing API corrections
version: 1.0.0
actions:
- target: "$.paths['/api/v1/invoices'].get.summary"
update: List paginated invoices
- target: "$.paths['/api/v1/invoices'].get.parameters[?(@.name=='internal_cursor')]"
description: Internal pagination cursor, not part of the public contract.
remove: true

Every overlay has an overlay: 1.0.0 version line, an info block, and an ordered list of actions. Each action carries a target — a JSONPath expression selecting the node to change — and exactly one operation:

  • update merges an object into the target (deep, with the overlay winning field by field), or replaces a scalar or array outright.
  • remove deletes the targeted node.

An action’s optional description is a note to your future self: Docuccino reads it and keeps it out of the document, so it’s the right place to record why a correction exists.

Point the document’s overlays config at one or more globs, relative to your application root. Files are applied in glob order, and the actions inside each file in the order written:

// config/docuccino.php — documents.default
'overlays' => [
'resources/docs/overlays/*.yaml',
],

The golden rule: overlays edit what already exists

Section titled “The golden rule: overlays edit what already exists”

Overlay 1.0 targets nodes in a document. A target that resolves to nothing is not an instruction to create it — the action simply has no effect (and Docuccino warns). So when you want to add something, merge onto the parent that exists rather than targeting the key that’s missing:

You want to add Don’t target Do target
A description on an operation ….get.description ….get, with update: {description: …}
An example on a response body ….content['application/json'].example ….content['application/json'], with update: {example: …}
A DELETE on a documented path ….['/api/v1/invoices'].delete ….['/api/v1/invoices'], with update: {delete: {…}}

The one thing this pattern doesn’t stretch to is a whole new path. $.paths and the document root are the two nodes that can’t carry an overlay’s provenance record, so targeting either produces a document that fails schema validation (document.schema-invalid) rather than a new path. Synthesizing paths is a DocumentTransformer job.

Overlays are applied at assembly time, once your operations have been built, as the overlay(45) layer in Docuccino’s precedence chain:

fallback(5) < inference(10) < integration(20) < docblock(30) < attribute(40) < overlay(45) < config(50)

So an overlay beats anything inferred, integrated, or written as a docblock or attribute: on the nodes it targets, it has the last word. Because merging is field-by-field, an overlay can rewrite one field of an operation without discarding the rest of it.

Every value an overlay changes is recorded in that node’s provenance with producer: overlay, and the value it replaced is captured under overrode — so the raw UIR always answers “why is it documented this way?”, even for a hand-applied correction.

Docuccino resolves a documented, predictable subset of JSONPath — enough to address any node in a real specification, with no ambiguous matches:

Selector Example Matches
Root $ The document root.
Dot member $.info.title A named object member. Letters, digits, _ and - only.
Bracket member $.paths['/api/v1/invoices'] A member whose key needs quoting — slashes, dots, spaces.
Array index …parameters[0] An array element by position.
Equality filter …parameters[?(@.name=='status')] Every list element whose string field equals a value.

Bracket notation is what you’ll reach for most: path keys ('/api/v1/invoices'), status codes (responses['200']) and media types (content['application/json']) all contain characters dot notation can’t express.

Anything outside this subset — wildcards (*), recursive descent (..), array slices ([1:3]), unions, or any comparison other than == — raises an error diagnostic rather than matching silently.

Nothing about an overlay fails quietly. Every problem shows up in the build output, so docuccino:export --fail-on=warning catches a rotten overlay in CI:

Code Severity Means
overlay.invalid warning The file couldn’t be parsed — a missing overlay: 1.0.x line, a non-list actions, or an action with no target or no operation. The whole file is skipped.
overlay.target-missing warning The target matched no node; that one action did nothing.
overlay.unsupported-selector error The target uses JSONPath outside the supported subset.
overlay.conflicting-operation error One action declares both update and remove. Split it in two.

A billing package ships its own /oauth/token route. You can’t annotate its controller — but you can document it. First let the route into the document (routes whose controller lives under vendor/ are excluded by default):

// config/docuccino.php — documents.default
'routes' => ['include' => ['api/*', 'oauth/*'], 'include_vendor' => true],

Then give it a real summary, a description, and a usable example:

overlay: 1.0.0
info:
title: Billing route polish
version: 1.0.0
actions:
- target: "$.paths['/oauth/token'].post"
update:
summary: Exchange credentials for an access token
tags: [OAuth]
description: |
Issues a bearer token for the client-credentials and password grants.
- target: "$.paths['/oauth/token'].post.responses['200'].content['application/json']"
update:
example:
access_token: "eyJ0eXAiOiJKV1Qi…"
token_type: Bearer
expires_in: 3600

Each update merges onto a node that already exists, so the operation’s inferred parameters, its other responses, and the response schema are all left untouched. Note the second target: the media-type object is what exists, so that’s where example is merged in.

Sometimes a route answers a verb Docuccino can’t see — it’s dispatched outside the framework router, or served by an edge function in front of your app. If the path is already documented, you can add the operation to it:

overlay: 1.0.0
info:
title: Invoice voiding
version: 1.0.0
actions:
- target: "$.paths['/api/v1/invoices/{invoice}']"
description: Served by the billing gateway, not by this app's router.
update:
delete:
summary: Void an invoice
tags: [Invoices]
responses:
'204':
description: The invoice was voided.

The merged operation is hand-authored: there’s no inference behind it, and no stable identity — so keep it small and factual, because you’re describing a contract by hand rather than deriving it. If you find yourself doing this for many operations, or you need a path that isn’t in the document at all, step up to a DocumentTransformer.

Remove something internal from the published spec

Section titled “Remove something internal from the published spec”

Inference documents what it finds, including the odd parameter or property you’d rather not publish. Prune them with remove:

overlay: 1.0.0
info:
title: Trim internals
version: 1.0.0
actions:
- target: "$.paths['/api/v1/invoices'].get.parameters[?(@.name=='debug_trace')]"
remove: true
- target: "$.components.schemas.Invoice.properties.internal_ledger_ref"
remove: true

Removing an array element re-indexes the array to a clean list, so a filtered-out parameter never leaves a gap — and a filter matching several elements removes them all, highest index first. For recurring, name-based hygiene across a whole document, the leakage lint is a better fit — it flags sensitive-looking properties everywhere they appear, rather than one target at a time.

An overlay targets specific nodes. When a change is really a policy — it should apply to every operation, or synthesize structure programmatically — reach past overlays:

  • Config and representation policies cover the common cases declaratively: operation-id style, filter naming, nullable style, tag mapping, error-response strategy. See the configuration reference.
  • A DocumentTransformer is a whole-document post-processor that runs after assembly and overlays, with the finished document in hand. It’s the right tool when you need to touch many operations at once, or build structure from data — generating a set of paths from a manifest, for instance. Every official integration is built with the same extension points — see writing an integration.