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.
Which tool for which change
Section titled “Which tool for which 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.
OpenAPI Overlays in practice
Section titled “OpenAPI Overlays in practice”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.
Anatomy of an overlay
Section titled “Anatomy of an overlay”overlay: 1.0.0info: title: Invoicing API corrections version: 1.0.0actions: - 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: trueEvery 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:
updatemerges an object into the target (deep, with the overlay winning field by field), or replaces a scalar or array outright.removedeletes 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.
Where overlays live
Section titled “Where overlays live”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.
When they apply — and what wins
Section titled “When they apply — and what wins”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.
The target selector
Section titled “The target selector”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.
Diagnostics
Section titled “Diagnostics”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. |
Worked examples
Section titled “Worked examples”Patch an operation you can’t edit
Section titled “Patch an operation you can’t edit”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.0info: title: Billing route polish version: 1.0.0actions: - 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"/oauth/token": { "post": { "summary": "Exchange credentials for an access token", "description": "Issues a bearer token for the client-credentials and password grants.\n", "tags": ["OAuth"], "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object" }, "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.
Add an operation Docuccino never saw
Section titled “Add an operation Docuccino never saw”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.0info: title: Invoice voiding version: 1.0.0actions: - 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."/api/v1/invoices/{invoice}": { "get": { "…": "inferred, untouched" }, "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.0info: title: Trim internals version: 1.0.0actions: - target: "$.paths['/api/v1/invoices'].get.parameters[?(@.name=='debug_trace')]" remove: true - target: "$.components.schemas.Invoice.properties.internal_ledger_ref" remove: trueRemoving 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.
Document-wide changes
Section titled “Document-wide changes”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
DocumentTransformeris 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.