Annotations you delete
Everything in the list above is inferred, so most parameter and response annotations come out on migration. What’s left are the genuine overrides — the facts your code doesn’t state.
Scramble is an excellent, popular API documentation generator, and if it’s working well for you, that’s a great place to be. The two tools share a core idea — documentation generated from your code, not hand-written annotations — so if you’ve been happy with Scramble’s approach, Docuccino will feel familiar. This page covers what’s different, what you’d gain, what you’d give up, and exactly how to move your annotations, config and extensions across.
Both tools read your code. Where they diverge:
| Scramble | Docuccino | |
|---|---|---|
| Primary artifact | An OpenAPI document | A UIR document, emitted as OpenAPI 3.2, 3.1 or 3.0, JSON or YAML |
| Analysis engine | Its own type inference over your code | An embedded PHPStan/Larastan engine, installed as a dev dependency |
| Production install | Ships with the analyser | Adapter only — no static-analysis toolchain in vendor/ |
| Interactive viewer | Yes | Yes, bundled Scalar |
| Output stability | Regenerated on demand | Byte-stable for identical code — built to be committed and reviewed |
| Contract checking | — | docuccino:diff over stable node identities, with --enforce gating a version policy in CI |
That last row is the reason most teams look at Docuccino. Because output is deterministic, the spec
becomes an artifact you commit; because every node carries a stable identity, Docuccino can tell a
rename apart from a removal and fail a pull request that breaks the contract without the version bump
your policy demands. See docuccino:diff.
Both tools generate from your code, and the overlap is large and healthy. The tables below break it
down capability by capability, with Scramble’s free core and Scramble PRO split out so you can see
which tier a capability lives in. ✓ means built-in support; — means not supported.
Scramble PRO is a separately-licensed paid add-on, so a ✓ in that column is a capability you buy
rather than one the free package ships; Docuccino is MIT-licensed with no paid tier, so its column is
one free package throughout. Prices change — check Scramble’s own site
for the current figures.
| Capability | Docuccino (free, MIT) | Scramble (free) | Scramble PRO (paid) |
|---|---|---|---|
| Validation rules → schema constraints | ✓ | ✓ | ✓ |
Form Requests and inline validate() |
✓ | ✓ | ✓ |
| Spatie Data objects | ✓ | — | ✓ |
| Laravel Actions | ✓ | — | ✓ |
| Spatie Query Builder filters, sorts, includes, fields | ✓ | — | ✓ |
| …recovered when the query is assembled in a helper method | ✓ | — | ✓ |
Docuccino maps 78 named Laravel rules onto schema constraints, and documents your own rule classes from
a #[RuleSchema] on the class, with a RuleTransformer
for anything it doesn’t know. Query builders are a genuine tie — Scramble PRO follows the builder out of
the route action too — so if that’s what brought you here, the difference is licensing, not capability.
| Capability | Docuccino (free, MIT) | Scramble (free) | Scramble PRO (paid) |
|---|---|---|---|
| API Resources | ✓ | ✓ | ✓ |
| Eloquent model schemas | ✓ | ✓ | ✓ |
| Backed enums, with case descriptions | ✓ | ✓ | ✓ |
Reusable components/schemas, deduped and $ref’d |
✓ | ✓ | ✓ |
| Pagination envelopes | ✓ | ✓ | ✓ |
| Path parameter types from route-model binding | ✓ | ✓ | ✓ |
JSON:API resources (timacdonald/json-api) |
✓ | — | ✓ |
…with the include and fields query parameters |
✓ | — | — |
| Spatie JSON API Paginate parameters | ✓ | — | ✓ |
Polymorphic MorphTo → discriminated oneOf |
✓ | — | — |
| Capability | Docuccino (free, MIT) | Scramble (free) | Scramble PRO (paid) |
|---|---|---|---|
| Security requirements derived from route middleware | ✓ | ✓ | ✓ |
| Sanctum in both token and stateful-cookie modes | ✓ | — | — |
Sanctum token abilities (x-abilities) |
✓ | — | — |
| Passport OAuth2 scopes, per operation | ✓ | — | — |
| Passport client-credentials (machine-to-machine) | ✓ | — | — |
Per-operation scheme override (#[Security], #[OptionallyAuthenticated]) |
✓ | — | — |
Role / permission requirements (x-permissions) |
✓ | — | — |
Rate-limit 429 responses + Retry-After / X-RateLimit-* headers |
✓ | — | — |
429 numbers folded out of RateLimiter::for closures |
✓ | — | — |
Both tools read the middleware stack to decide whether an operation is protected. Docuccino goes on to read how: it resolves the guard’s driver, so a custom guard name still lands on the right scheme, and it carries abilities, scopes and permissions through to the operation.
| Capability | Docuccino (free, MIT) | Scramble (free) | Scramble PRO (paid) |
|---|---|---|---|
Framework defaults (422 / 401 / 403 / 404) |
✓ | ✓ | ✓ |
Exceptions thrown in the action, and abort() helpers |
✓ | ✓ | ✓ |
| Your real error shapes, read from your exception handling | ✓ | — | — |
Error examples, with const-pinned literal members |
✓ | — | — |
| RFC 9457 Problem Details preset | ✓ | — | — |
Reading your actual exception handling is where inference goes furthest, and it’s covered in detail
below. Scramble documents the exceptions it can see being thrown —
validate(), authorize(), abort(), missing bound models, and @throws one level deep — and leaves
the body of a custom error to you. Both approaches get you a documented error response; the difference
is whether you hand-maintain its shape beside the code that produces it.
| Capability | Docuccino (free, MIT) | Scramble (free) | Scramble PRO (paid) |
|---|---|---|---|
| Interactive viewer | ✓ | ✓ | ✓ |
| Multiple documents / API versions | ✓ | ✓ | ✓ |
| Build cache | ✓ | ✓ | ✓ |
| Deterministic, byte-stable output | ✓ | — | — |
| A spec artifact you commit and review in a pull request | ✓ | — | — |
| Semantic diff over stable node identities | ✓ | — | — |
CI gate on breaking changes (docuccino:diff --enforce) |
✓ | — | — |
| A documented intermediate format (the UIR) | ✓ | — | — |
| No static-analysis toolchain in a production install | ✓ | — | — |
Everything down to the build cache you can have either way. The rows below it are the ones worth weighing, because they only exist for structural reasons: output is deterministic, every node carries a stable identity, and the analyser is a dev dependency the adapter names by string rather than a hard requirement. See what Docuccino doesn’t do for the other side of the ledger.
The tables give the shape; this is what the inference actually covers. Whatever you’re migrating from, this is the surface you get without writing an annotation — the useful list to check your own app against:
validate(), and Spatie Data objects, with the full Laravel
rule vocabulary (78 named Laravel rules) mapped to schema constraints.@property docblocks, Data objects, backed enums with case descriptions, pagination
envelopes, JSON:API resources, and polymorphic MorphTo returns as a discriminated oneOf.components/schemas, deduped and $ref’d.x-abilities, Passport OAuth2 scopes per operation and client-credentials
flows, and role/permission requirements as x-permissions.401 / 403 / 404 / 422 shapes, plus your real error bodies
read out of your exception handling, and an RFC 9457 Problem Details preset behind one config line.429 responses with Retry-After and X-RateLimit-* headers, the numbers folded
out of your RateLimiter::for closures.Error handling deserves a closer look, because it’s where inference goes furthest. Docuccino reads
render callbacks, exception render() methods and Responsable exceptions — and where a member of
your error body is statically knowable (a per-branch type URI, a title, a status that echoes the
response) it pins that member as a schema const and includes it in a generated example, following
helper indirection and enum accessors to find it. Members it can’t resolve are left out rather than
invented. Documentation you’d otherwise hand-maintain alongside the code that produces it.
Annotations you delete
Everything in the list above is inferred, so most parameter and response annotations come out on migration. What’s left are the genuine overrides — the facts your code doesn’t state.
Registration timing stops mattering
Register extensions from any service provider, in register() or boot(), in any order.
Nothing resolves until a build starts, so an extension can’t be missed by registering it late.
The spec stops drifting
Commit the output and gate CI on docuccino:diff. Deterministic bytes mean a real change is
the only thing that shows up in the diff, and --enforce fails the build when a breaking
change ships without the version bump your policy requires.
Most annotations map across cleanly.
| Scramble | Docuccino | Notes |
|---|---|---|
#[QueryParameter] |
#[QueryParameter] |
Same idea; Docuccino fills the rest by inference. |
#[BodyParameter] |
#[BodyParameter] |
Patches one property of the inferred body. |
#[PathParameter] |
#[PathParameter] |
Adds an OpenAPI format. |
#[HeaderParameter] |
#[HeaderParameter] |
— |
#[CookieParameter] |
#[CookieParameter] |
Direct swap. |
#[Group] |
#[Group] |
Tag assignment; repeatable. |
#[Example] / @example |
#[Example] |
On an action, pins the success response’s example body. An @example docblock line on a Data-class property carries a field-level example. |
| Exclude a route / controller | #[ExcludeFromDocs] |
Method or class target. |
@unauthenticated |
#[Unauthenticated] |
Clears the inferred security requirement. |
@response {status} |
#[Response] |
Patches field-level, keeping inferred siblings. |
Hidden fields / $hidden |
#[Hidden] |
Property-level on Data classes; class-level list on models; your model’s $hidden / $visible are read anyway. Hides from output. |
| A field hidden from a request schema | #[HiddenFromRequest] |
Drops a Data-class property from the request body only, leaving the response schema alone. |
| Enum case descriptions | #[CaseDescription] |
Emitted as x-enumDescriptions. |
| Markdown description file | #[DescriptionFromFile] |
Loads a symbol-anchored Markdown file into description. |
| Deprecation | #[DeprecatedOperation] |
Marks an operation (or controller) deprecated, with an optional reason. |
Docuccino also adds attributes with no direct Scramble equivalent — #[OperationId], #[Internal],
#[InDocs], #[IgnoreParam], #[IgnoreResponse], #[ResponseHeader], #[SchemaId], #[SchemaName],
and the security family #[Security], #[OptionallyAuthenticated] and #[Abilities]. See the full
attributes reference.
Docuccino’s config is organized around documents — each an independent output with its own
routes, info, security, and viewer — so a few top-level Scramble settings move under
documents.default.
Scramble (config/scramble.php) |
Docuccino (config/docuccino.php) |
|---|---|
api_path: 'api' |
documents.default.routes.include: ['api/*'] |
api_domain |
documents.default.routes.closure (or an include glob) |
export_path |
documents.default.export.path |
info.version / info.description |
documents.default.info.version / .description |
servers |
documents.default.servers |
ui.title |
documents.default.info.title |
| viewer / UI options | documents.default.viewer (route, gate, middleware, source) |
enum_cases_description_strategy |
#[CaseDescription] (emitted automatically as x-enumDescriptions) |
extensions |
extensions, or Docuccino::extend() |
| security via config/callbacks | documents.default.security + the Sanctum/Passport integrations |
| a single document | as many named documents as you need |
See the configuration reference for every option.
The extension model is similar in spirit; the contracts live in
Docuccino\Core\Extensions\Contracts.
| Scramble | Docuccino |
|---|---|
| Type-to-schema extension | TypeToSchema |
| Operation extension | OperationExtension (phased) |
| Exception-to-response extension | ExceptionToResponse |
| “After document generated” callback | DocumentTransformer |
| Custom validation-rule handling | RuleTransformer — or, for one rule class, #[RuleSchema] with no extension at all |
The Writing an integration guide walks through building one end-to-end.
Docuccino is new, and there are things it deliberately or simply doesn’t do today. Check this list against how you work before you commit:
#[Example], never from running your code. There is no
factory-rendered or live-response example generation — that’s the trade for a generator that needs
no database and has no side effects.docuccino:export, or point the viewer at
source: generate so it rebuilds per request while you
work locally.Rule:: objects fold statically — Rule::enum
(including ->only() / ->except() chains), Rule::in, Rule::exists and Rule::unique —
alongside the whole string-rule vocabulary; your own rule classes document from a
#[RuleSchema] on the class; and a
RuleTransformer teaches Docuccino any rule you invent. But a closure rule, a Rule::when()
conditional, or a rule class carrying no attribute has opaque logic, so Docuccino raises a diagnostic
naming the field rather than guessing at its shape.You don’t have to switch in one go. Docuccino writes to its own configured path and serves its viewer on its own route, so nothing collides:
php artisan docuccino:export --out=docs/openapi.docuccino.jsonCompare it against your existing Scramble output, adopt Docuccino where you’re happy with what it produces, and remove the annotations it makes redundant as you go.