Skip to content

Docuccino vs Scramble

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:

  • Requests — Form Requests, inline validate(), and Spatie Data objects, with the full Laravel rule vocabulary (78 named Laravel rules) mapped to schema constraints.
  • Query parameters — Spatie Query Builder filters, sorts, includes, fields and pagination, traced through helper methods several calls deep, so a query assembled across a few methods (or a separate query object) still documents fully.
  • Responses — API Resources (including conditional and closure fields), Eloquent model schemas from casts and @property docblocks, Data objects, backed enums with case descriptions, pagination envelopes, JSON:API resources, and polymorphic MorphTo returns as a discriminated oneOf.
  • Reusable components — DTOs, models, resources, request bodies and enums hoisted to components/schemas, deduped and $ref’d.
  • Path parameters — typed from the bound model’s route key (uuid, ulid, integer, string).
  • Authentication — middleware-driven, per guard driver: Sanctum in both token and stateful-cookie modes, token abilities as x-abilities, Passport OAuth2 scopes per operation and client-credentials flows, and role/permission requirements as x-permissions.
  • Errors — the framework’s own 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.
  • Rate limits429 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.

Three things that change about your workflow

Section titled “Three things that change about your workflow”

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:

  • No Postman collection export. Docuccino emits OpenAPI (JSON or YAML) and the UIR; import the OpenAPI file into Postman.
  • Examples come from inference and #[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.
  • No watch mode. Re-run docuccino:export, or point the viewer at source: generate so it rebuilds per request while you work locally.
  • Closure rules aren’t documented. Built-in 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:

Terminal window
php artisan docuccino:export --out=docs/openapi.docuccino.json

Compare 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.