Diagnostics reference
Diagnostics are how Docuccino tells you it knows less than your code does, or that something you
configured didn’t take. They’re printed by docuccino:export, docuccino:validate, docuccino:cache
and docuccino:watch, grouped by route and in a stable order, and they can be embedded in a UIR
document with --embed-diagnostics.
Every one carries a code — the stable identifier in the first column below — plus a message naming the exact class, route or file it’s about, and usually a line of help. This page is the exhaustive list. If you’re working back from a symptom rather than from a code, start at Troubleshooting.
Every code a build reports prints a link to this page beneath it, the first time that code appears in a run — so a code you met in your terminal is one click from what it means.
Severities
Section titled “Severities”A diagnostic’s severity is what --fail-on gates on: pass it a floor and anything that loud or
louder exits non-zero.
| Severity | What it means |
|---|---|
| Error | Something is missing from the document, or the document is invalid. Always worth fixing. |
| Warning | Something you wrote didn’t take effect, or the output is less useful than you asked for. |
| Info | Docuccino recovered less than your code says and widened to stay truthful. Normal in small numbers; the same code on every action is a signal. |
| Hint | The quietest level: a note an extension chose not to make louder. Nothing Docuccino itself emits today. |
# Fail CI on anything that isn't a recovery note.php artisan docuccino:export --fail-on=warning
# Tighter: gate on inference certainty too, once the info list is short.php artisan docuccino:export --fail-on=infoOn an existing codebase, start at warning and tighten to info later. A gate that fires on day one
is a gate the team switches off.
Accepting a code
Section titled “Accepting a code”The other way to tighten a gate is to accept the codes you can’t act on. List them under
diagnostics.accept in docuccino.yaml and they
keep printing — marked accepted, with a hit count at the end of the block — while --fail-on stops
counting them:
diagnostics: accept: ['eloquent.no-columns', 'validation.rule-unrecoverable'] [info, accepted] eloquent.no-columns: Model App\Vendor\Ledger exposes no documentable columns. Accepted, so --fail-on ignores them: eloquent.no-columns (12)Acceptance changes the exit code and nothing else: the document is byte-identical either way, and the
report is still in the log the day it starts firing somewhere new. An error is never accepted —
it says the document is wrong or the build lost a whole tier of facts, and an entry that reaches one
is reported as config.accept-refused while the run fails as it always would. An entry nothing
reports is reported as config.accept-unused, so the list can’t outlive what it was for.
The engine and inference
Section titled “The engine and inference”Docuccino reads your types with an embedded static analyzer. These codes are about the analyzer itself — whether it ran, and where reading your code hit a bound.
| Code | Severity | What it means | What to do |
|---|---|---|---|
engine.not-installed |
warning | No analyzer is present, so the document came from docblocks and attributes only | Install the engine: composer require --dev docuccino/inference-phpstan. Set DOCUCCINO_ENGINE=null if you meant to document without inference |
engine.boot-failed |
error | The analyzer is installed and couldn’t start, so the document came from docblocks and attributes only | Generate from the project root, in an environment your app boots in — see the analyzer won’t start. The message quotes what it threw |
engine.mode-unknown |
warning | engine.mode isn’t a mode Docuccino has; it ran in-process anyway |
Set DOCUCCINO_ENGINE to in-process or null — see Engine |
inference.action-failed |
warning | Analyzing one action threw | One or two: annotate those actions. All of them: you probably have a version mismatch |
inference.method-not-found |
warning | An action’s method has no body the analyzer can read — it’s abstract, or resolved to something that isn’t a method | Point the route at a concrete method, or state the response with #[Response] |
inference.callable-failed |
warning | Analyzing a callable the trace followed — a render callback, a query-object method — threw | The message names it. Simplify it, or state the shape it produces with an attribute |
inference.callable-not-found |
info | A callable the trace wanted to follow has no readable body | Usually vendor code, and usually fine. If it’s yours, express it as a plain method the analyzer can reach |
inference.response-shape-truncated |
info | Recovering a response shape ran out of descent depth or file budget, so the response is documented as its bare declared type — true, but poorer than your code is | Flatten the chain between the return and the value it builds — every project-code call on the way is one hop, and no config key raises the bound. That is the only thing that clears it: #[Response] corrects the document and this notice keeps naming the callable |
inference.http-exception-status-unread |
info | A throw carries an HTTP status the analyzer could not read, so the error is documented without one and a later tier classifies it. Names the exception, the file and line the throw is written at, and which fold gave up — a status handed to abort() that isn’t a constant, a construction that folds to more than one status, a class that states none. Only ever where you can act: a status written in vendor/ is never named, because the only fix would be an edit you don’t own |
Write the status as a constant where the throw is — a literal and a class constant both fold, and so does the constructor default a construction leaves the slot empty for. A status chosen at run time is not one: this build cannot tell which of them the response is. Pin it in the class with parent::__construct(409, …) if every instance is that status, and otherwise write it at the throw, at the abort(), or in the static factory the throw names |
inference.descend-scope-narrowed |
info | You set engine.project_paths, and it excludes a source root your composer.json declares under autoload — so a call that can throw was not opened and an error behind it is documented nowhere. Names the callee, the line it is called at, and the file descent stopped short of. Only ever where deleting the key would reach the file: never unset, never for vendor code, and never for a root only autoload-dev declares, since none of those is a scope you chose |
Delete engine.project_paths from docuccino.yaml to descend into every PSR-4 root you declare under autoload, which is the default, or add this file’s directory to the list you keep |
inference.ambiguous-narrowing |
info | More than one return site is reachable for the narrowed type, so the first in source order was used and the recovered shape may be ambiguous | Give the action one return site per status, or pin the ones you care about with #[Response] |
Routes, operations and names
Section titled “Routes, operations and names”The document has one operation per path and method, and one component per name. These codes fire where two things want the same slot, or where a name reached the document that a client can’t use.
| Code | Severity | What it means | What to do |
|---|---|---|---|
route.build-failed |
error | Documenting one route threw, so it was omitted or replaced by a skeleton operation | The message quotes the failure and names the route. Fix it, or exclude the route from the document |
route.duplicate-operation |
error | Two routes resolve to the same operation identity — their paths differ only by a parameter’s name, which isn’t part of an identity — so a semantic diff pairs them as one operation | Give one of them a path that differs by more than a parameter name |
route.operation-collision |
error | Two routes claim one path and method, usually the same URI on two hosts, and OpenAPI has room for one | Remove the duplicate registration, or give each host its own document — see routes bound to a host |
route.duplicate-operation-id |
warning | Two routes publish one operationId, so a generated client names one function for the pair |
Give one its own id with #[OperationId], or name the routes distinctly |
route.fallback-omitted |
info | A Route::fallback() catch-all was left out: its path stands for every unmatched request, not an endpoint |
Nothing, usually. Document the not-found body as a 404 on the operations that produce one — see Fallback routes |
route.unmatched-exclusion |
warning | A withoutMiddleware() removed nothing and named no alias the build could resolve — so either the route never carried it, or it is another spelling of a middleware the route does carry and the responses behind it are documented without being enforced |
Check the spelling, or write both sides in the same one — the class name on both, or the alias on both. See Opting out of middleware |
route.middleware-registrations-unreadable |
warning | The application’s middleware registrations could not be read, because the HTTP kernel that writes them to the router could not be resolved. Nothing a route inherits from a middleware group is visible then: a group’s throttle publishes no 429 and no rate-limit headers, and a group’s authenticator publishes no 401 and no security requirement, which documents a protected route as public. Middleware written on the route itself is unaffected |
Settle whatever makes Illuminate\Contracts\Http\Kernel unresolvable — the message quotes the container’s own reason. A dev-only class named in bootstrap/app.php’s middleware block is the usual cause of one that only fails in production |
route.middleware-aliases-unreadable |
warning | The framework’s own default middleware alias table could not be read, so middleware is resolved through whatever aliases the router itself holds — nothing at all where the HTTP kernel could not be resolved either. A middleware named by its class is then no longer equated with its alias | Report it, quoting the message: the installed laravel/framework does not carry the table this version reads |
route-binding.column-untyped |
info | A {invoice:reference} binding names a column nothing types, so the parameter is a plain string rather than the column’s type |
Add a @property tag (or a $casts entry) for that column on the model — see Path parameters |
route-binding.untyped |
info | A path parameter is bound to a class nothing enabled could type — a custom UrlRoutable, an int-backed or pure enum (Laravel binds string-backed enums only), or an Eloquent model with the eloquent integration off — so it is documented as a plain string |
Declare the segment’s type with #[PathParameter], or bind a string-backed enum or an Eloquent model with the eloquent integration enabled |
tags.name-collision |
info | Two controllers of the same short name derived one default tag, so their operations group together | Nothing, if that grouping is what you meant. Otherwise split them with #[Group] or tags.map |
components.name-collision |
warning | Two components asked for one name — two schemas, or the shared API version header and something already holding the name it asks for — so each was published under a name of its own — derived from its namespace, or from a hash of its content where there was no namespace to walk | Nothing, if those names read well. A hashed one rarely does: name them yourself with #[SchemaName]; for an error body, #[ErrorComponent] where it arises from a throw, or #[Response(errorComponent:)] where the operation declares it. A name reaches the shape under a response only where the response states one representation. The message lists every claimant and the name it got. For the version header, rename the component holding the name or name the header something else with api_version.header |
components.example-name-conflict |
warning | Two operations sharing one error body give the same example name to different examples, so neither example kept that name: each publishes on the shared response under it plus a hash of its own content, rather than one key meaning two things | Rename one of them, or make them agree, and the name is published as you wrote it. The message names the key and the component the examples went to |
components.name-invalid |
warning | A shared error-response name an OpenAPI component key can’t carry reached the document — from an overlay, since a producer’s own name is refused where it’s written — so the body was published under its status instead | Fix the name: a component key is letters, digits, ., _ and - only. The message names whatever the document says wrote it |
document.schema-invalid |
error | The assembled document failed validation against the UIR schema | Report it — this is a bug in Docuccino, not in your app. The message gives the JSON pointer and the rule that failed |
document.transformer-failed |
error | A document transformer threw. Every other transformer still ran and the document was still written, so what it holds may be neither what that transformer would have made of it nor what it was before | Report it, quoting the message — a built-in transformer throwing is a bug in Docuccino, one of your own or a package’s is a bug there. Use --fail-on=error where a pipeline must refuse to ship the document meanwhile |
Responses recovered from your code
Section titled “Responses recovered from your code”Docuccino widens rather than guesses. Each of these says which response got vaguer and why.
| Code | Severity | What it means | What to do |
|---|---|---|---|
inferred-response.payload-unrecoverable |
info | An action returns a bare framework response whose body the document still can’t describe — a JSON body of unrecovered shape, or a stream whose media type nothing states | Build the payload where the analyzer can see it, or name the body with #[Response(type: …)] — which silences it, as does #[IgnoreResponse]. A stream needs type: and mediaType:: a media type is what it lost, and mediaType: alone documents nothing to put under it. See framework response objects and file downloads and streams |
inferred-handler.too-dynamic |
warning | Your exception handler couldn’t be folded to a fixed response shape for some exception types, so those errors are documented without the shape it renders — and where Docuccino can see your handler replaced the framework’s body, with no body at all, which a contract test on a response that really returns bytes will fail | Return a JsonResponse from the arm — response()->json(…), not a plain response(), a view or a redirect — with the payload written at that call site and a literal integer status (404, not $e->getCode()). The payload is what settles it: where only the status or the Content-Type folds, the response publishes that much and no shape. #[Response(status: …, type: …)] publishes the shape instead, and this warning keeps naming the callback. See Error responses |
inferred-handler.render-callback-skipped |
info | A render callback couldn’t be analyzed and was skipped, so its error responses fall through to the next tier |
Register the renderer as an invokable object, an [$object, 'method'] pair, or a closure typed to the exception it handles |
validation.rule-unrecoverable |
info | A validation field’s rules are a closure, a custom rule object with no #[RuleSchema], or a conditional descriptor, so the field is documented from its type alone — or omitted where it has no type either |
Express the field with recoverable rules, or annotate the rule class with #[RuleSchema] |
validation.rule-values-unread |
info | A rule states a value that isn’t written at the rule — it comes from a call, a variable or a spread — so the constraint is left off and the field keeps the rules that did recover | Write every value where the rule is (Rule::in('draft', 'live')), which is what settles it — a partial list would make a generated client reject a value the API accepts. An overlay corrects the document instead, and this notice keeps naming the rule |
validation.rule-unhandled |
info | No transformer handled a validation rule, so the property stays permissive | Nothing, for rules with no schema meaning. For one that should constrain the schema, register a rule transformer — see Documenting a custom rule |
validation.container-undecided |
info | A field’s array rule has no item or key rules under it. array is Laravel’s one word for both containers, so a JSON array and a JSON object both satisfy the field, and it is documented as either |
Add field.* rules for a list, or dotted field.<key> rules for an object, and the document states the one the endpoint means. A #[BodyParameter] naming a key inside the field answers it too, as does one naming the field with a type: that says which shape it is — object for a free-form map with no keys to enumerate. A spatie Data property states it in its type instead — list<string>, array<string, mixed> — which the build reads. Leaving it is sound: what is published is true, only wider than the endpoint |
Attributes
Section titled “Attributes”An attribute that couldn’t be applied never fails the build — it says so here and the document keeps what it would have had.
| Code | Severity | What it means | What to do |
|---|---|---|---|
attribute.body-on-bodyless-status |
warning | A #[Response] names a body under a status HTTP forbids one on, so the body wasn’t documented |
Document it under a status that may carry one, or drop type: — 1xx, 204, 205 and 304 never carry content |
attribute.body-parameter-name |
warning | A #[BodyParameter] whose name is not a field path — it has an empty segment, from a leading, trailing or doubled dot, or the name is empty — so no property was documented |
Write the name the way a validation rule key is written: nickname, meta.validation_overrides, items.*.id. A dot that belongs to the field name itself is escaped \. |
attribute.body-parameter-parent |
warning | A #[BodyParameter] whose name nests under a field the body can’t nest into — a scalar, an allOf/anyOf/oneOf, or a $ref to a shared component — so no property was documented. The message names the field and what it’s documented as |
For a scalar, document the parent as an object first with a #[BodyParameter] naming it and type: 'object', or name a top-level field instead. For a shared component, declare the property where the component is defined — every operation using it would inherit one added here — or patch this one body with an overlay |
attribute.error-component-invalid |
warning | An error component name an OpenAPI component key can’t carry — from an #[ErrorComponent] or from a #[Response(errorComponent:)] — so it named nothing and the response kept the name it would have had. The message names the declaration to fix |
Fix the name: letters, digits, ., _ and - only. A reason phrase as one word — NotFound, TooManyRequests — reads best as a generated client’s type. See Name your own errors |
attribute.error-component-contested |
warning | Two exceptions one action signals name the same status’s response differently, and a response carries one name, so the default stands | Keep the attribute on the exception that response really is, give the errors statuses of their own, or name the body on the render method that builds it |
attribute.error-component-unread |
warning | An #[ErrorComponent] on an action, which names nothing: the attribute names an error where the error is defined, not where an operation answers with it. Reported for the action’s own declaration only — one inherited from a controller would say the same thing on every route under it |
Move it to the exception class the error is raised from, or to the render method that builds the body. For one status of this operation, use the errorComponent: argument of the #[Response] that declares it |
attribute.error-component-unreachable |
warning | A #[Response(errorComponent:)] whose name reached nothing: the status shares no error body (anything below 400), or the status is a $ref to a component that was named where it is defined |
For a non-error status, move the name to the error status it describes or drop it — a success body’s schema is named after the class type: points at. For a referenced one, name the component at its own definition |
attribute.hidden-unmatched |
warning | A class-level #[Hidden('…')] names no property the schema publishes, so it hid nothing — and where the name went stale through a rename, the field it was written to keep out is published under the new spelling. The message lists what the schema does publish. Not raised for an Eloquent model: a model’s documented columns are recovered from @property tags, $casts and $fillable rather than declared, so a name outside them is far more often an undocumented column than a typo |
Correct the name against the list in the message, or delete the declaration. A Data class hides by the property’s own name, not by the key #[MapName] publishes it under. See #[Hidden] |
attribute.mock-invalid |
warning | A #[Mock] that can publish nothing — no faker expression and no seed group, or a class-level one that doesn’t name its property |
Fill it in or delete it. The message names the exact class or property — see Mock data hints |
attribute.mock-unknown-property |
warning | A class-level #[Mock(property: '…')] names a member the schema doesn’t publish, so the hint was dropped |
Fix the name, or unhide the property. Columns, toArray() keys and validated fields are all named as they appear on the wire |
attribute.example-unusable |
warning | An #[Example] that doesn’t describe one example — no value, or more than one, or more than one target, or a nameless one sharing a node with named ones |
Give it exactly one value and at most one target; the message names which half is wrong. See #[Example] |
attribute.example-target-missing |
warning | An #[Example] points at something the operation doesn’t document — a status, a media type, a parameter, a request body |
Document the target first with #[Response], #[BodyParameter] or a parameter attribute, or point the example somewhere it is |
attribute.example-duplicate-name |
warning | Two #[Example] declarations on one node share a name, and a name is a map key, so the second was dropped |
Give each its own name — the message says which name and which node |
attribute.in-docs-unknown |
warning | An #[InDocs] names a document key that isn’t configured, so the key pins nothing — and where a declaration names no configured key at all, #[InDocs] is an allow-list, so every route or webhook under it is left out of every document. The message names the routes it is written on and lists the documents that do exist. Raised once per key, however many routes the declaration covers |
Correct the key against the list in the message, or delete the declaration — a document renamed in documents.* keeps its old key only in the attribute. To keep a route out of every document on purpose, use #[ExcludeFromDocs]. See #[InDocs] |
attribute.ignore-param-location |
warning | An #[IgnoreParam] whose in: names no parameter location, so it dropped nothing |
Use cookie, header, path or query — in any case — or leave in: off to drop the name from every location. See #[IgnoreParam] |
attribute.ignore-param-unmatched |
warning | An #[IgnoreParam] on an action names a parameter the operation doesn’t document, so it dropped nothing and the field the author meant to hide — if it is there under another name — is still published. The message lists what the operation does document. Reported for the action’s own declaration only: one inherited from a controller names a key only some of its actions take, and an action that never documented it is not a mistake |
Correct the name against the list in the message, or delete the declaration. A parameter that was renamed keeps its old spelling only in the attribute; a key only some actions take belongs on the class. See #[IgnoreParam] |
attribute.ignore-response-unmatched |
warning | An #[IgnoreResponse] on an action names a status no producer would have written, so it dropped nothing. The message lists the statuses the operation does document. Reported for the action’s own declaration only, for the same reason attribute.ignore-param-unmatched is |
Correct the status against the list in the message, or delete the declaration — a status only some actions answer with belongs on the class. An ignore names one status, so a response published as a range (3XX for an unpinned redirect) is not one it can drop. See #[IgnoreResponse] |
example-file.missing |
warning | An #[Example(file: …)] names a file that isn’t there |
Create it or fix the path, which is read relative to your application root. Docuccino watches it either way, so the example appears the moment the file does |
example-file.invalid |
warning | An #[Example(file: …)] file didn’t decode — not .json/.yaml/.yml, unparseable, or empty |
Fix the file; the message quotes the parser |
example-file.escapes-base-path |
error | An #[Example(file: …)] path doesn’t name a path inside your application — it leaves the root, or holds a character no filesystem can — so nothing was read |
Write the path relative to the application root; a path that leaves it is refused by design |
attribute.description-unusable |
warning | A #[Description] that documented nothing: it carries both text: and file:, or neither, so it says nothing certain — or it carries request: true on an operation with no request body, so there is nothing for it to describe |
The message says which. Give it exactly one of text: and file:; for the request form, document the body first or drop request: to describe the operation instead. See #[Description] |
attribute.path-parameter-unmatched |
error | A #[PathParameter] names no {segment} of the route’s own URI. OpenAPI requires every in: path parameter to correspond to a template variable, so the parameter is withheld rather than published — an invalid document is not something a generated client can read, and a path parameter no request can carry describes nothing the server accepts, so nothing true is lost by leaving it out. The message lists the segments the template does have. Reported for the action’s own declaration only: one inherited from a controller covers a segment some of its actions have, and an action without it is not a mistake — it is withheld there too, so the document stays valid either way |
Correct the name to a segment of the route’s URI, or delete the declaration. A parameter that is not in the URI is #[QueryParameter], #[HeaderParameter] or #[CookieParameter]. See #[PathParameter] |
attribute.property-unsupported |
warning | A declaration says something the schema it sits on has no field for — a #[Description(file: …)] on a property or a class (no application root reaches a schema), a #[Description(request: true)] on either (a request body is one operation’s use of a type, not part of it), or an #[Example] naming what only an Example Object holds (name:, status:, request:, parameter:, file:, externalValue:) |
Say it in the field that exists: #[Description(text: '…')] for the prose, a bare #[Example(…)] value for the example. Everything else belongs on the action — see #[Example] |
attribute.schema-class-unread |
warning | A Docuccino attribute on a request type — a Form Request, a DTO, a Data class — that nothing reads on a TYPE. TARGET_CLASS means an action class or a schema class, and an action’s declarations are read off the action; PHP accepts them on the type too, where nothing does. The message names the class and the attribute, and the remedy says where that attribute IS read. Raised once per attribute, so a repeatable one written twice is one report |
Move it to where it is read — the action, for an operation-level declaration. A type is read for #[Description], #[SchemaName], #[SchemaId], #[Hidden], #[Mock] and #[BodyParameter] |
attribute.schema-class-unusable |
warning | A #[BodyParameter] on a request type that no operation could read. It is honoured on a type — but only where the route documents a request body, and a read verb (GET, HEAD) sends the same validation rules to query parameters instead. Reported once per type, and only when NO operation built from that type documents a body: a type shared by GET /things and POST /things carries a declaration that is doing its job on the POST, so nothing is said about it |
Describe what a read route accepts with #[QueryParameter] on the action, or delete the declaration. If a write route was meant to accept the type, the missing route is the real fix. See #[BodyParameter] |
attribute.unreadable |
warning | A Docuccino attribute at the named declaration could not be instantiated — usually a typo’d argument — so it was ignored | Check the arguments at that declaration against the attribute’s constructor; the message names what it threw |
description-file.missing |
warning | A #[Description(file: …)], or info.description.file in config, names a file that isn’t there |
Create it or fix the path, which is read relative to your application root. Docuccino watches it either way, so the description appears the moment the file does |
description-file.escapes-base-path |
error | A #[Description(file: …)], or info.description.file in config, doesn’t name a path inside your application — it leaves the root, or holds a character no filesystem can — so nothing was read |
Write the path relative to the application root — see symbol-anchored prose |
Docblock tags
Section titled “Docblock tags”A docblock tag can only hold text, so a value written in one has to be read back as the type it describes. Where the text doesn’t read as that type, nothing is published — an example of the wrong type would be copied out of the document and rejected by your own API.
| Code | Severity | What it means | What to do |
|---|---|---|---|
docblock.example-untypable |
warning | An @example (or a #[RuleSchema(example: …)]) doesn’t read as the type declared beside it — @example n/a on an int, say — so no example was published |
Write the example as the value the property really is: @example false for a boolean, @example 7 for an integer, a JSON literal (@example ["a", "b"]) for an array or object. The message names the property, the type and the text |
Configuration
Section titled “Configuration”Everything Docuccino read out of your configuration and couldn’t use. Every code here names the exact key, or — for the four that are about the file itself — the file.
| Code | Severity | What it means | What to do |
|---|---|---|---|
config.file-invalid |
error | docuccino.yaml isn’t configuration this can read — not valid YAML, or valid YAML whose aliases expand to more settings than a configuration holds — so nothing in it was applied. Every command that builds a document refuses rather than building one from defaults, and the refusal doesn’t go through --fail-on |
Fix the line the message names. A duplicate key counts: YAML refuses two settings of the same name rather than quietly keeping the last. An expansion is an alias (*name) repeated inside an anchor: write the settings out |
config.file-not-a-map |
error | docuccino.yaml holds something other than a map of settings — most often it’s empty, or everything in it is commented out. Every command that builds a document refuses rather than building one from defaults |
Write the settings as top-level key: value pairs, or delete the file: no file at all is a supported state, and an empty one is not the same thing |
config.file-unreadable |
error | docuccino.yaml is there and couldn’t be read. Every command that builds a document refuses rather than building one from defaults |
Check the file’s permissions |
config.file-misnamed |
warning | There’s no docuccino.yaml, but there is a file that was obviously meant to be it — docuccino.yml, a dotfile, a .dist template — and it was not read. A warning rather than a refusal, because there is no configuration file: the document built from defaults is what an application with none is supposed to get |
Rename it to docuccino.yaml. That’s the one name configuration is read from, so there’s no precedence to work out |
config.not-migrated |
error | There’s no docuccino.yaml, and config/docuccino.php still holds build settings — which nothing reads. Building from defaults would have thrown away the routes, info and security you configured and produced a plausible-looking document, so every command that builds one refuses before it starts. That refusal doesn’t go through --fail-on: the flag gates what a build found, and this is the configuration not being read at all. docuccino:migrate-config still runs, because it’s the way out |
Run php artisan docuccino:migrate-config to write docuccino.yaml from the settings already there, then delete them from config/docuccino.php |
config.stale-php-keys |
warning | docuccino.yaml says what your documents are, and config/docuccino.php still holds build settings beside it. Nothing there is merged or given precedence — they were ignored |
Delete them. config/docuccino.php keeps enabled, each document’s viewer and cache.store, and nothing else. If those settings never made it into docuccino.yaml, php artisan docuccino:migrate-config --force writes them there first |
config.unknown-setting |
warning | A key in docuccino.yaml names no setting, so nothing written under it does anything — a misspelling, or a block one indentation level too far in, which moves a whole bag under the wrong parent. The message names the key as you wrote it, and the help names the one setting you probably meant: the same name at the depth it belongs at, a near miss by spelling, or the fact that it’s read from config/docuccino.php instead |
Fix the key, or delete it. A bag under an unknown key is reported once rather than once per line, so the count is the number of mistakes. Nothing is reported inside info, servers, security.schemes, security.default, security.document, tags.map, lint.leakage.patterns, representation.examples.formats or query_builder.filter_descriptions, where the names are yours |
config.viewer-orphan |
warning | config/docuccino.php configures a viewer for a document docuccino.yaml doesn’t define. Boot can’t know that, so the routes are registered and every request to them fails |
Add the document to docuccino.yaml, or delete its viewer entry. A document with no viewer is fine — an export-only document is an ordinary shape |
config.viewer-route-collision |
warning | Two documents configure the same viewer.route, and the framework keeps only the last one registered — so one document’s page is unreachable |
Give each document its own viewer.route |
config.value-type |
warning | A key in docuccino.yaml holds a value of the wrong shape, and it was refused rather than converted, so the setting fell back to its default. Usually YAML read the value as something other than what it looks like: version: 1.10 is the number 1.1, and enabled: no is the text no rather than false. Every key the shipped file declares is checked against the shape that file writes there — a section holding a line of text, a list holding one, a text key holding a number. The two spellings of the same mistake are why: routes: 'api/*' and routes: { include: 'api/*' } both leave no route filter at all, so every route in the application is documented |
Do what the message says — usually quote it, write false/true, write the section as key: value pairs indented under its name, or write the list as one - entry per line. Converting the value would be worse than refusing it: 1.10 would publish as 1.1, a different version number, and a non-empty no would come out true. An on/off switch is reported by config.not-a-switch instead, and error_responses by config.unknown-error-responses — one line to fix gets one report |
config.value-not-finite |
warning | A setting holds .nan, .inf or -.inf. No document can carry those, so the setting was read as empty and fell back to its default |
Write a finite number, or drop the key |
config.extension-missing |
warning | An entry in extensions names no autoloadable class, or isn’t a class-string at all, so that extension contributed nothing |
Fix the class name and namespace, and check it’s autoloadable. InvoiceExtension::class still evaluates to a string when the class is missing, so a typo is otherwise silent |
config.engine-config-missing |
warning | engine.config names an analyzer config file that isn’t there, so the analyzer ran without whatever it registers |
Fix the path — it’s read relative to your application root — or drop the key. See Engine |
config.unknown-integration |
info | An integrations.<key> bag names no integration, so nothing reads it |
Fix the key. The message suggests the one you probably meant, and Integrations lists them all |
config.enabled-ignored |
info | You set enabled on an integration that’s always on and has no toggle |
Delete the key |
config.machine-dependent-value |
warning | A value your clients act on — an OAuth flow URL, a session cookie name — came from the build machine rather than from anything you pinned, and was published as-is | Pin it in docuccino.yaml — see Pin the values your clients act on |
config.machine-dependent-path |
info | A configured path points outside your application, so it’s folded into the document’s hash verbatim and the output stops being portable between machines | Move the target inside the application; in-app paths are stored relative to the base path |
config.path-rejected |
warning | A configured path holds a NUL byte, which no filesystem path can hold, so nothing read it — an overlay wasn’t applied, a content tree wasn’t compiled, the fragment cache stayed off | Write the path in single quotes, or escape the backslash. "\0" in a double-quoted PHP string is a NUL byte, not the two characters it looks like |
config.not-a-switch |
warning | A key that is an on/off switch holds something that is neither true nor false, so the switch could not be honoured and its default was used instead |
Write true or false. Any other value is refused rather than coerced — 'no' and 'off' are strings, and a coerced string is true, so coercing one would turn the switch on |
config.route-filter-unusable |
error | routes.filter names something that can’t be applied — not a class-string, not autoloadable, not a RouteFilter, or it threw while the container built it — so the run stopped rather than documenting every route the globs admitted |
Fix the class name, or implement the contract — see routes. Skipping the filter would publish a route set you had narrowed on purpose, so nothing is written until it works |
config.tag-mapper-unusable |
warning | tags.mapper names something no tag mapper could be got from — not a class-string, not autoloadable, not a TagMapper, or one the container could not build — so tags are published exactly as your controllers and #[Group] attributes wrote them |
Fix the class name, or implement the contract — see Tags. The message says which of the four it was. Unlike routes.filter this doesn’t stop the run: unmapped tags are still your API’s real tags, where a route set you narrowed on purpose is not |
config.unknown-value |
warning | A setting whose values are a closed set — error_responses, tags.default_strategy, the representation policy keywords, versioning, viewer.source, on_route_error — holds a value that is none of them, so the setting fell back to its documented default. Usually a mistyped keyword, or one YAML read as something other than text: 1.10 is the number 1.1, and a bare date is a number too |
Write one of the values the message lists. It names the setting, what you wrote and the value used instead; the accepted values for each setting are in Configuration |
config.unknown-tag-parent |
info | A tag in tags.definitions is parented to a tag no definition declares, so the link was dropped |
Declare the parent, or remove the parent key. OpenAPI requires a parent tag to exist |
config.tag-parent-cycle |
info | A tags.definitions parent link closes a cycle, so it was dropped to keep the hierarchy a tree |
Re-parent one of the tags in the cycle the message names |
config.tag-mapper-unhashable |
info | tags.mapper resolved to a class declared in no file — eval()’d code, say — so the fragment cache can’t tell whether its answer has changed and rebuilds every operation it tags |
Declare the mapper as an ordinary autoloadable class in a file of its own. Only raised when the fragment cache is on, because with it off there is nothing to rebuild |
extension.unhashable |
info | A registered extension is declared in no file — eval()’d code, say — so the fragment cache can’t tell whether its body has changed, and is off for the whole document: an extension shapes whatever operations it is run over, and nothing can say which |
Declare the extension as an ordinary autoloadable class in a file of its own. Only raised when the fragment cache is on, because with it off there is nothing to rebuild |
config.unknown-filter-kind |
info | A key in integrations.query_builder.filter_descriptions names no Query Builder filter kind, so the sentence under it is never used |
Use one of the kinds the message lists — see overriding the generated prose |
config.format-sample-rejected |
warning | A sample in representation.examples.formats couldn’t be published — a field’s own validation rules reject it, or it isn’t a string at all — so that field falls back to the built-in sample |
Use a value every field carrying that format accepts; the message names the format, your value and the keyword it failed — see representation |
config.export-no-targets |
error | export.targets is set but holds no entries, so the document has nowhere to go |
Remove the key to fall back to export.path, or list at least one {format, path} entry |
config.export-target-shape |
error | An export.targets entry isn’t a {format, path} pair with both members set to a non-empty string |
Fix the entry — see Export targets |
config.export-unknown-format |
error | An export target names a format Docuccino doesn’t emit | Use one of the formats the message lists |
config.export-duplicate-format |
error | Two export targets name the same format, and --format and the viewer’s artifact both have to resolve to one file |
Keep one target per format |
config.export-duplicate-path |
error | Two export targets in one document write the same file, so the later would clobber the earlier | Give each target its own path |
config.export-yaml-unsupported |
error | An export target asks for a .yaml file in a format that has no YAML serialization |
Give it a .json path rather than a .yaml file holding JSON |
config.export-path-collision |
error | Two documents write the same file, so one would clobber the other | Give each document its own export path — see Multiple documents |
config.accept-refused |
warning | A code in diagnostics.accept was reported as an error, and acceptance never covers an error, so it still failed the run |
Fix what the error reports, then delete the entry — see Diagnostics |
config.accept-unused |
warning | A code in diagnostics.accept that nothing reported: the cause is fixed, or the code is misspelled |
Delete the entry. Checked only once a run has built every document, so a single-document run never raises it |
config.export-path-ignored |
info | A document sets both export.targets and export.path, and targets win, so the path is never written |
Delete the export.path key |
integration.disabled |
info | An integration’s package is installed, but the integration is off — either opt-in and never switched on, or explicitly disabled | Set integrations.<key>.enabled = true if you want its contributions. The message names the package and the key |
Package integrations
Section titled “Package integrations”What each package integration couldn’t recover from your code. Every one of these degrades to a correct-but-vaguer document rather than dropping the endpoint.
| Code | Severity | What it means | What to do |
|---|---|---|---|
eloquent.no-columns |
info | A model exposes no documentable columns, so its response is a bare object | Add @property (or @property-read) tags for the model’s attributes — e.g. @property int $id — so its columns and their types are recovered |
eloquent.custom-date-serialization |
info | A model overrides serializeDate(), so its date attributes’ wire format isn’t statically known and they’re documented as plain strings |
Nothing you can annotate: no attribute carries a column format, and a docblock type has no format to state. If clients need an exact one, state it in an overlay — which corrects the document, and this notice keeps naming the model |
eloquent.unresolved-eager-load |
info | A relation named in $with couldn’t be resolved to a related model, so it’s omitted from the schema |
Give the relation method a generic return type — e.g. HasMany<LineItem, $this> |
eloquent.unmapped-morph |
info | A morph variant has no Relation::morphMap() alias, so the union is emitted without a discriminator |
Register an alias in Relation::enforceMorphMap([...]) for every variant, so a stable discriminator can be emitted |
query-builder.unresolved-entry |
warning | A Query Builder allow-list entry couldn’t be resolved statically, so it’s omitted from the docs and that list is documented as a plain string rather than a value enum | Use a literal value or a factory call — AllowedFilter::exact('status') — so it can be recovered |
query-builder.no-allowlists-recovered |
info | A paginating terminal was reached and no allow-lists and no default sort were recovered from the chain, so it is behind an indirection the trace couldn’t follow | Declare the allow-lists somewhere the trace reaches — see query objects. An endpoint whose chain WAS read and simply offers no filters or sorts doesn’t report this |
query-builder.partial-on-enum |
info | A partial-match filter over an enum-cast column can’t have its values enumerated | Use AllowedFilter::exact() so the enum’s values are documented — see partial filters over an enum column |
query-builder.default-config |
info | The package’s config wasn’t readable, so documented parameter names use its defaults (filter/sort/include/fields) |
Publish it — php artisan vendor:publish --tag=query-builder-config — so custom names reach the docs |
query-builder.legacy-package-version |
info | spatie/laravel-query-builder below v7 is installed, so the sort/include/fields allow-lists are documented as plain strings rather than value enums | Upgrade to spatie/laravel-query-builder ^7 to document the sort/include/fields allow-lists as enums |
query-builder.enum-name-collision |
info | Two sort/include/fields values would share one SDK enum member name, so distinct value-derived names were published instead | Rename one of the colliding allow-list entries so each value mints its own member name |
spatie-data.nested-collection-wrap |
warning | A nested collection of Data objects will be sent as {"data": [ … ]} because data.wrap is set, while the document describes a bare array |
Unwrap the property with a #[WithTransformer], or state the wrapped shape in an overlay — see Nested collections |
spatie-data.response-status-unresolved |
info | A Data class’s calculateResponseStatus() doesn’t fold to constant statuses, so the success response is documented as 200 |
Return constant ints, or a ternary whose arms are both constant — see Success statuses |
spatie-data.unknown-mapper |
info | A Data class uses a name mapper Docuccino doesn’t recognize, so its property names are documented unmapped | Use one of the package’s own mappers, or rename the properties |
json-api-paginate.default-config |
info | The package’s config wasn’t readable, so documented pagination parameters use its defaults (page[number]/page[size]) |
Publish it — php artisan vendor:publish --tag=json-api-paginate — so custom names reach the docs |
rate-limit.unregistered-limiter |
info | A route throttles on a named limiter nothing registers with RateLimiter::for(), so the allowance can’t be documented |
Register it in a service provider, or state the allowance inline as throttle:60,1 — see Named limiters |
rate-limit.multiple-throttles |
info | A route carries more than one throttle middleware; one 429 is documented from the first |
Nothing. The others are still enforced — they just aren’t separately representable in OpenAPI |
Authorization
Section titled “Authorization”A route’s authorization is read from its middleware, so the 403 is documented from the presence of a
gate. Usually that is the whole story; where it is not, this says so and the response stays put.
| Code | Severity | What it means | What to do |
|---|---|---|---|
authorization.gate-cannot-deny |
info | A route’s ->can() gate resolves to a policy method whose whole body is return true;, and nothing else on the route produces a 403 — so the error the operation publishes is one no request can provoke, and it reaches a consumer as a dead catch branch in their generated client |
Tighten the policy method, so the 403 describes something that can happen — or, if the gate is deliberately a formality, drop the response with #[IgnoreResponse(403)] on the action. The response is published either way: dropping a real error needs certainty a build cannot have, so this reports and changes nothing |
Only a literal, unconditional return true; counts — or, where an analyser is installed, a body every
one of whose returns it can prove is true. A method that reads anything at all — the user, a request,
ambient state, a helper — can deny, and the notice stays quiet even where the body looks decorative:
return currentTeam() instanceof Team; names neither a user nor a permission and denies perfectly well.
It is quiet, too, wherever the gate’s own answer is not the last word — a Gate::before or Gate::after
hook, a policy before() method, a signed or verified middleware beside the gate, a FormRequest
that authorizes, or a 403 your action throws for itself.
The notice names the class the ability method is declared in, which is not always the policy the gate
resolved to: an inherited or trait-provided method is written somewhere else, and that is the file to
open. For the same reason it says nothing about a method declared under vendor/ — a policy a package
ships, or a base class one of yours extends. The only remedy there would be an edit to somebody else’s
code, so there is nothing to report.
Both spellings of the middleware are read: ->can('view', Post::class) and
Authorize::using('view', Post::class).
Security schemes
Section titled “Security schemes”Checked over the finished document, so overlays, transformers and config all count. A
security requirement names its scheme by key rather than by
$ref, so nothing else in the pipeline resolves it.
| Code | Severity | What it means | What to do |
|---|---|---|---|
security.undefined-scheme |
error | An operation or the document names a security scheme that components.securitySchemes never defines. OpenAPI requires every requirement to name a declared scheme, so the document is invalid and a generated client has nothing to build the credential from |
Add the scheme under this document’s security.schemes, or name one it already defines — the message lists them. The reference is still published: dropping it would leave a valid document saying the endpoint is public, which is the one answer worse than an invalid one |
security.undeclared-scope |
warning | An OAuth2 requirement asks for a scope none of that scheme’s flows declare, so the document contradicts itself and an authorization server following it would refuse the token | Declare the scope under that scheme’s flows.*.scopes, or ask for one it offers — the message lists them |
Webhooks
Section titled “Webhooks”Webhooks are collected from #[Webhook] classes under the directory you configure. See
Webhooks.
| Code | Severity | What it means | What to do |
|---|---|---|---|
webhook.dir-missing |
warning | The configured webhook directory doesn’t exist, so no webhooks were collected | Create it, or unset webhooks.dir — see pointing the document at your webhook classes |
webhook.dir-escapes-base |
warning | The webhook directory doesn’t name a path inside your application and was ignored | Write it relative to the application root |
webhook.name-invalid |
warning | A class carries a #[Webhook] with no name, so it isn’t in the document — a webhook is published under its name |
Give the attribute the name the receiving endpoint subscribes to, e.g. #[Webhook('invoice.paid')] |
webhook.name-collision |
error | Two classes claim one webhook name and method, so one of them isn’t in the document | Give one a name of its own — a webhook name is the contract a consumer subscribes to |
webhook.operation-collision |
error | A webhook already documents that method from another class, so this one isn’t in the document | Give one of them its own name, or a method the other doesn’t use |
webhook.method-unknown |
warning | A #[Webhook] asks for an HTTP method OpenAPI has no path-item member for, so it’s documented as POST |
Use one of the methods the message lists |
webhook.payload-unresolved |
warning | A webhook’s payload type resolves to no shape, so its body is an unconstrained object | Name a class or array shape the payload is built from, or drop the payload argument to document the annotated class itself — see Annotate the payload. payload: 'object' resolves: in an attribute that word is the JSON one, and it documents a free-form map |
webhook.build-failed |
error | Documenting one webhook threw, so it isn’t in the document | The message quotes the failure and names the webhook |
API versions
Section titled “API versions”Codes from the version changes a document declares under
api_version. They fire while an older version’s
document is derived from the code you have today — see
API versioning.
| Code | Severity | What it means | What to do |
|---|---|---|---|
versioning.dir-missing |
warning | A configured version-changes directory doesn’t exist — or a glob entry matched nothing — so its changes were not read and every version publishes today’s shape for them | Create it, or drop the entry from api_version.changes |
versioning.dir-escapes-base |
warning | A version-changes entry doesn’t name a path inside your application — or its wildcard matched one that doesn’t — and was ignored | Write it relative to the application root, and keep what its wildcards match inside the application |
versioning.change-invalid |
warning | A change can’t be applied as it’s written — no version on its #[ApiVersionChange], an empty or self-referential rename, a verb leaving schema: or field: empty, a rename onto a field or parameter an operation already has, an in: that names no parameter location, a #[RenamedParameter] naming in: 'path', or a version the document’s order can’t read |
Fix the declaration: to: is the name in the code today, from: the one older versions publish, and every other verb names its field exactly as the code spells it today. A parameter’s in: is one of cookie, header or query, in any case — a path parameter is named by the URL template as well as by the parameter, so nothing can rename it on its own. Where the version itself is the problem, write it the way the document writes its own |
versioning.change-target-missing |
warning | A change names something the document no longer has — a field the schema doesn’t publish, on either half of the wire, or a parameter no operation in scope declares — so that version is left saying what the code says | Update the change to the name as it’s spelled today, or retire it if the field or parameter is gone |
versioning.change-target-unchanged |
warning | A change says a field’s required-ness moved, or that a field was removed, and the schema already publishes it the way the older version would — so there was nothing to undo and that version says what the code says. Weaker than the rename’s check by nature: “remove from required” done twice is a no-op, so this can only tell you the code disagrees with the declaration, never that the edit already ran |
Read the direction: #[MadeResponseFieldRequired] says the field is required in the code TODAY and was optional before, the two Optional verbs say the reverse, and #[RemovedResponseField] names a field the code no longer has. Retire the declaration if the field has changed again since |
versioning.type-unresolved |
warning | A #[RemovedResponseField] states a type: that’s neither a class this document publishes a response schema for nor an OpenAPI type name, so the field went back with no shape at all |
Name a class this document publishes, or one of string, integer, number, boolean, object, array — each may be suffixed [] for a list of them or ? for one that may be null. Leave type: out to publish the field unconstrained on purpose |
versioning.schema-unresolved |
warning | A change names a class this document publishes no schema for — or, for a request verb, no request body schema for — so it was skipped | Name the class whose shape the document actually publishes — a change can only rewrite a schema the document contains |
versioning.version-unstated |
warning | A document declares api_version but writes no info.version, so there’s nothing to derive a version from |
Set info.version to the version this document describes — that value is the API version |
versioning.unordered |
warning | The versions in play are neither all dates nor all semver, so the changes couldn’t be placed in order and none was applied | Write every version the same way, and set versioning to date or semver |
versioning.scope-matches-nothing |
warning | An #[AppliesTo] names no operation this document publishes that schema for, so that part of the change applies to nothing — including the case where this document publishes the schema for no operation at all. For a parameter rename, which names no schema, it means the selector names no operation this document publishes at all |
Write the operation as the document names it — GET /api/things, an operationId, or either with a * — and check this document publishes that schema for it. Nothing is renamed: a scope that reaches nothing never widens into a document-wide rename |
versioning.scope-unforkable |
warning | A scoped change matched an operation it can’t be narrowed to — the schema contains itself, so no private copy of it can be written, or the operation is published through a path item shared with operations the scope leaves out, so both operations are one node. The operation stays at the shape the code publishes, parameter names included | Drop the #[AppliesTo], or widen it to every operation that publishes the schema — or, for a shared path item, to every operation behind it — and the shared node is edited in place instead |
versioning.example-dropped |
warning | A version moved a field of a schema — a response one or a request one — and an example published beside it couldn’t be given the shape this version publishes — a rename the walk couldn’t follow, or a #[RemovedResponseField(required: true)] putting back a field no example can carry — so it was dropped rather than published failing its own schema |
A consumer copies an example and sends it back, so one the schema rejects is worse than none. A rename’s rewrite stops where the schema doesn’t settle on one shape for the example — a oneOf/anyOf branch, a $ref that leads back to itself, a value that isn’t the kind of thing the schema describes, or an example already carrying both names. Pin an example that matches the schema beside it, narrow the schema so one shape governs it, or drop required: true if that version didn’t always send the field |
Narrative content
Section titled “Narrative content”Codes from the Markdown pages you fold into the document. See Adding your own pages.
| Code | Severity | What it means | What to do |
|---|---|---|---|
content.dir-missing |
warning | The configured content directory doesn’t exist, so no pages were compiled | Create it, or unset content.dir |
content.dir-escapes-base |
warning | The content directory doesn’t name a path inside your application and was ignored | Write it relative to the application root |
content.frontmatter-not-a-switch |
warning | A markdown page’s frontmatter sets a switch — nav.hidden — to something that is neither true nor false, so it was refused and the default used |
Write true or false. Frontmatter is YAML, where no, off, yes and on are strings rather than booleans |
content.duplicate-slug |
error | Two content pages share a slug, so the later one is ignored | Rename one of them; a slug is a page’s address |
content.duplicate-operation-id |
warning | Two operations share an operationId, so an ::operation directive naming it resolves to the last one in path order |
Give one its own id with #[OperationId] |
content.unresolved-directive |
error | An ::operation or ::schema directive is missing its attribute, or points at something the document doesn’t have |
Point it at a documented operation id, METHOD /path, or component schema name — see Linking to your API |
content.unknown-directive |
warning | A directive Docuccino doesn’t resolve was left in the page untouched | Nothing, if your renderer handles it. Docuccino resolves ::operation and ::schema |
content.unresolved-nav-ref |
error | A page’s nav frontmatter references something that resolves to nothing | Fix the reference — see the frontmatter reference |
Overlays
Section titled “Overlays”Codes from OpenAPI Overlays. See Customizing the output.
| Code | Severity | What it means | What to do |
|---|---|---|---|
overlay.invalid |
warning | An overlay file couldn’t be read as an overlay, so it was skipped entirely | The message quotes the problem. Check the file parses and carries the overlay members |
overlay.conflicting-operation |
error | An overlay action declares both update and remove, and an action carries exactly one operation |
Split it into two actions |
overlay.unsupported-selector |
error | An overlay target uses a selector Docuccino doesn’t resolve | Rewrite it with the supported subset: object members, array indexes, and [?(@.field=='value')] equality filters — see the target selector |
overlay.target-missing |
warning | An overlay target matched no node, so the action did nothing | Check the target against the document you’re overlaying — overlays edit what already exists |
Recorded examples
Section titled “Recorded examples”Codes from examples your test suite recorded. See Examples your tests recorded.
| Code | Severity | What it means | What to do |
|---|---|---|---|
examples.recordings-empty |
info | The recordings directory holds no recordings, so the document publishes none | Recording is opt-in per assertion: pass recordAs: at the assertions whose responses you want published, and run your suite with the recorder registered. Or drop examples.recordings |
examples.recordings-escapes-base |
warning | examples.recordings doesn’t name a path inside your application — it leaves the root, or a symlink in it does — so the directory was never read and no recorded examples were published |
Write it relative to the application root, or point it at a directory inside the application |
examples.recording-unreadable |
warning | A file in the recordings directory isn’t a recording Docuccino can read, or records an operation its filename doesn’t name | Re-record it, or delete it. The message names the file |
examples.recording-orphaned |
warning | A recording is for an operation this document no longer has — the route was renamed, moved or removed | Delete the file, then re-record whatever replaced it. The message names the endpoint it came from |
examples.recording-unsafe |
warning | A committed recording still holds what looks like a credential, so it wasn’t published | Re-record it; a credential string is replaced on the way out. A number is reported and left alone, because a placeholder where the schema says integer would make the example contradict its own contract — stop returning it. If the value really is public, list the pointer the message names under lint.leakage.allow — a bare property name silences the lint but never the redaction. The message names the pointer, never the value |
examples.recording-unnamed |
info | A committed recording holds a body no assertion named — a file from before recording became opt-in. It still publishes, and no run will ever refresh it | Pass recordAs: at the assertion that produces that body and the next run replaces it, or delete the file if the endpoint no longer needs an example |
Lint rules
Section titled “Lint rules”Document-quality rules. lint.data-leakage is on by default; the rest are opt-in — see
Lint.
| Code | Severity | What it means | What to do |
|---|---|---|---|
lint.data-leakage |
warning | A schema property, a query or path parameter, or an example or default value looks like a credential |
Take a property out of the shape that publishes it — #[Hidden] for a response, #[HiddenFromRequest] for a request body. Carry a parameter’s value in a header instead, since a URL is recorded in access logs and browser history. Or safelist either under lint.leakage.allow; see Data leakage |
lint.missing-description |
warning | An operation publishes neither a summary nor a description, so the document never says what it does | Give the action a docblock — its first line becomes the summary — or write one in an overlay. See Descriptions |
lint.operation-id-style |
warning | An operationId a generated client can’t name a method after: empty, leading with a digit, or outside letters, digits and . - _ @ |
Give it an id with #[OperationId], or rename the route. See Operation ids |
lint.example-mismatch |
warning | A published example doesn’t satisfy the schema it sits beside — a string where the type says boolean, a number under a minimum |
Correct the example, or widen the schema if the example is what your API really accepts. Where the example is right and the schema merely under-describes it, accept the pointer under lint.examples.allow |
lint.example-uncheckable |
warning | An example was left unchecked because the validator wouldn’t read the schema beside it, so this build knows nothing about that example either way | Nothing you can fix in your own code: a schema no validator will read is a bug in Docuccino. Report it with the message, which names the schema’s pointer and what the validator objected to. lint.examples.allow accepts the pointer meanwhile |
lint.unresolved-reference |
warning | A path item, response or request body is written as a $ref to a component the document doesn’t define, so nothing downstream can read what it promises — a generated client, a viewer and a contract test all see an empty node |
Define the component the pointer names, or correct the pointer. An overlay that renamed or removed a component is the usual cause |
lint.unpinned-redirect |
info | An operation’s document never says exactly one thing about its redirect: the 3XX range stands alone, so it never says which redirect it is, or the range stands beside a concrete 3xx, so it says both |
Name the code with #[Response(302)], which declares it and retires the range together. A code named in an overlay needs a second action removing the 3XX response, since an overlay is applied after the document is built. Either shape is accepted as-is under lint.unpinned_redirect.allow |
lint.vacuous-union |
info | An anyOf in the operation carries an unconstrained branch ({}, or the true that means the same), so the value validates as anything and the typed branches beside it add no constraint |
Pin the arm that recovered as “anything” — a return docblock or #[Response] — or accept the operation under lint.vacuous_union.allow |
lint.undocumented-tag |
warning | Operations carry a tag tags.definitions never declares, so it publishes without the summary, description and parent the declared ones have |
Add an entry for it, or map it onto a declared tag with tags.map or #[Group]. See Undocumented tags |
Emitting OpenAPI 3.1 and 3.0
Section titled “Emitting OpenAPI 3.1 and 3.0”Docuccino’s document is OpenAPI 3.2-shaped. Exporting openapi-3.1 or openapi-3.0 rewrites what
those versions spell differently and drops what they can’t express — each one saying which member, at
which JSON pointer. None of them mean you did anything wrong: they’re the price of the older target,
and keeping the 3.2 artifact alongside costs you nothing.
Where a code is info or warning, the quieter one means Docuccino rewrote the construct into
something equivalent and the louder one means it had to drop it.
downlevel.empty-responses is the one that isn’t only about the older target: it reports a gap in the
document itself. Every OpenAPI version rejects a present-but-empty responses: {} — 3.1 and 3.2
included — and what they allow instead is leaving responses out altogether, which 3.0 does not. So the
placeholder is the 3.0 emitter’s alone, while the missing responses it stands in for are worth naming
whichever version you ship.
A downlevel.* code is raised once per emitted format that raises it, by the emitter doing the
rewriting — nothing dedupes across formats. downlevel.empty-responses comes from the 3.0 emitter only,
so it appears once in a build exporting 3.0 and not at all in one exporting 3.2, 3.1 or Postman. A 3.0
export chains through the 3.1 emitter, so it also carries every code the 3.1 target raises. --format=uir
emits no report of this kind at all.
| Code | Severity | What it means | What to do |
|---|---|---|---|
downlevel.query-method |
warning | The 3.2 query HTTP method has no 3.1 spelling, so the operation isn’t in the emitted document |
Keep the 3.2 artifact for consumers that need it |
downlevel.additional-operations |
warning | The 3.2 additionalOperations member has no 3.1 spelling and was dropped |
Model custom methods with a standard method for 3.1 toolchains |
downlevel.tag-summary |
warning | A tag’s 3.2 summary was dropped; 3.1 tags fall back to their name for display |
Nothing, or lead the tag’s description with the same sentence |
downlevel.tag-parent |
warning | A tag’s 3.2 parent was dropped. Where the document also carries the hierarchy as x-tagGroups — which a generated document does — only the native member is lost; where it doesn’t, the hierarchy flattens |
Nothing, when x-tagGroups is there. Otherwise nest the naming instead — “Billing / Invoices” |
downlevel.tag-kind |
warning | A tag’s 3.2 kind was dropped, so 3.1 consumers treat every tag the same |
Nothing |
downlevel.member-not-in-3.1 |
warning | A member OpenAPI 3.2 added, which 3.1 and 3.0 have no word for, was dropped where it stood — the message names it and its JSON Pointer. Covers the streaming members (itemSchema, prefixEncoding, itemEncoding, and a media type’s own description), an Encoding Object’s nested encoding, prefixEncoding and itemEncoding, an example’s dataValue and serializedValue, a response summary, a server name, a security scheme’s deprecated, an OAuth deviceAuthorization flow, and the components.mediaTypes bucket — whose members are inlined where a $ref named them, so no reference dangles |
Keep the 3.2 artifact for consumers that need the member; nothing a 3.1 or 3.0 document can say replaces it |
downlevel.value-not-in-3.1 |
warning | A VALUE OpenAPI 3.2 added to a member’s domain, which 3.1 and 3.0 don’t define — the member exists at every version and only the value is 3.2’s, so it is a different loss from downlevel.member-not-in-3.1 above. Two of them today. An in: querystring parameter is dropped whole: there’s no 3.1 way to describe the raw query string as one value, and it takes content rather than schema, so it isn’t a query parameter under another name. A shared one is reported where it’s defined and every $ref naming it goes with it, and a parameters member the drop empties is removed rather than left empty. A cookie parameter’s style: cookie costs only the member: the parameter stands at form, the one cookie style 3.1 spells |
Keep the 3.2 artifact for consumers that need the parameter — nothing a 3.1 or 3.0 document can say replaces it. The cookie style needs nothing written in its place: form is 3.1’s default there and both styles default explode to true, so what the older artifacts lose is RFC 6265 escaping and the ; an exploded value joins on, neither of which any explode value restores |
downlevel.webhooks |
warning | webhooks was dropped; OpenAPI 3.0 doesn’t define it |
Keep the 3.1 or 3.2 artifact for consumers that need the webhook contract |
downlevel.component-path-items |
info | components.pathItems was dropped; OpenAPI 3.0 doesn’t define it, so each path item a $ref names is inlined where it stands |
Nothing |
downlevel.path-item-ref |
info | A $ref to a shared path item was inlined at its use site; the message names every hop it followed |
Nothing |
downlevel.path-item-unresolved |
warning | A $ref chain reached no shared path item — the last hop names one the document doesn’t define, or the chain returns to a name it already followed — so the path was dropped rather than left pointing at a member the 3.0 artifact no longer carries. A 3.0 consumer loses the endpoint |
Define the shared path item the message names, or break the cycle it traces; either way one of them has to describe the path rather than point at another |
downlevel.info-summary |
warning | info.summary was dropped; OpenAPI 3.0 doesn’t define it |
Lead info.description with the same sentence |
downlevel.license-identifier |
info or warning | The SPDX info.license.identifier became info.license.url, or was dropped where a url was already set |
Nothing |
downlevel.mutual-tls |
warning | A mutualTLS security scheme was dropped, along with every requirement naming it |
Document mutual TLS in prose for 3.0 consumers, or keep the 3.1 artifact |
downlevel.empty-responses |
info | An operation documents no responses, and OpenAPI 3.0 requires every operation to declare at least one, so the 3.0 artifact carries a placeholder default response that describes no status or body |
Name what the endpoint returns — #[Response], a return docblock, or an overlay — so every artifact carries the real shape |
downlevel.multi-type |
info or warning | A multi-type type became an anyOf of single-type branches, or was dropped where the schema already composes |
Nothing |
downlevel.null-type |
warning | A null-only type became an untyped nullable: true; OpenAPI 3.0 has no null type |
Nothing, though 3.0 consumers see a looser schema |
downlevel.nullable-composition |
info | A {type: null} branch moved onto its parent as nullable: true |
Nothing |
downlevel.ref-siblings |
info | OpenAPI 3.0 ignores a $ref sibling, so members beside one were hoisted into an allOf wrapper — or, for a reference’s own summary/description, dropped in favour of the referenced component’s wording |
Nothing |
downlevel.const |
info | A const became a single-value enum, which is how OpenAPI 3.0 pins a value |
Nothing |
downlevel.content-encoding |
info or warning | contentEncoding: base64 became format: byte, or was dropped where a format was already set |
Nothing |
downlevel.exclusive-bound |
warning | A numeric exclusiveMinimum/exclusiveMaximum was dropped, because 3.0 spells it as a boolean on a bound that’s already taken |
Nothing, though 3.0 consumers see an unbounded number |
downlevel.schema-examples |
info or warning | The first of a schema’s examples was kept as example, or they were dropped where none could be |
Nothing |
downlevel.boolean-subschema |
info | A boolean schema — true for “anything goes”, false for “nothing is valid” — became the OpenAPI 3.0 spelling of the same constraint, {} or {"not": {}}. 3.0 takes a boolean at additionalProperties and nowhere else |
Nothing; the constraint is unchanged |
downlevel.unsupported-keyword |
warning | A JSON Schema keyword OpenAPI 3.0 doesn’t define was dropped from a schema | Keep the 3.1 or 3.2 artifact for consumers that validate against the full constraint |
Servers
Section titled “Servers”Facts about the servers a document publishes. Every
emitter that writes a URL resolves them for itself, so a code here is raised once per emitted
format — a build exporting OpenAPI 3.2, 3.1, 3.0 and a Postman collection reports the same variable
four times, once against each. --format=uir publishes the variables as configured and reports
nothing, since nothing has resolved a URL yet.
| Code | Severity | What it means | What to do |
|---|---|---|---|
server.variable-no-default |
warning | A server variable declares no default, which every OpenAPI version requires of one. Where the variable declares an enum, its first value stands in — that set is your API’s own, so a member of it resolves the URL to something you serve. Where it declares neither, an OpenAPI export leaves the variable out rather than resolve the URL to a value nobody serves, and a Postman collection publishes it blank |
Give the variable a default — one of its enum values, where it declares an enum. See servers |
Postman collections
Section titled “Postman collections”A Postman collection describes requests a person sends, so it carries less than an OpenAPI document does. See Postman collections.
| Code | Severity | What it means | What to do |
|---|---|---|---|
postman.no-server |
warning | The document declares no servers, so the collection’s baseUrl is empty |
Declare a server, or fill the variable in once after importing |
postman.variable-name-collision |
warning | A server variable is named baseUrl, which the collection already uses, so it isn’t published as a variable of its own |
Rename the server variable |
postman.path-template-partial |
warning | A path segment templates only part of itself, and a Postman path variable stands for a whole segment, so the segment was left literal | Edit the URL after importing, or template the whole segment |
postman.auth-unsupported |
warning | A security scheme has no Postman equivalent, so requests are sent unauthenticated | Add the credential by hand in Postman |
postman.auth-multi-scheme |
warning | An operation requires more than one credential together, and a Postman request carries one | Supply the others by hand — the message names which one the collection sends |
postman.body-not-object |
warning | A form body isn’t an object, and a form body is a list of fields, so it’s sent empty | Fill the body in after importing |
postman.body-unresolved |
warning | A request body’s schema is a $ref the document doesn’t define, so there’s no shape to build a body from and the request is sent empty |
Define the component the pointer names, or correct the pointer — an overlay that renamed or removed a schema is the usual cause |
postman.body-media-type |
warning | No example body could be built for a media type, so requests using it are sent empty | Add an example for that media type — see Example payloads |
postman.examples-truncated |
warning | An operation documents more saved responses than the collection keeps, so the first few by status were saved to stay navigable | Nothing. The full set is still in the OpenAPI document |
postman.callbacks-dropped |
warning | The callbacks an operation declares have no Postman equivalent | Nothing |
postman.webhooks-dropped |
warning | A collection describes requests you send, so it can’t carry the webhooks your API delivers | Nothing. Export an OpenAPI format alongside for the webhook contract |
postman.yaml-ignored |
warning | A Postman collection has no YAML form, so JSON was written | Give the target a .json path |