Path parameters
Requests
Docuccino documents everything an endpoint accepts — path parameters, query parameters, and request bodies — by reading your routes and validation rules statically. It never executes your rules or constructs your objects, so generating docs has no side effects.
Query parameters
Request bodies
File uploads
multipart/form-data automatically.Path parameters
Section titled “Path parameters”Every {param} in a route’s URI becomes a path parameter. Docuccino types it from the route itself:
- A parameter bound to a model through route-model binding is typed from the model’s route key:
a
uuid- orulid-formatted string for aHasUuids/HasUlidsmodel, anintegerfor the default incrementing key, or a plainstringfor a non-incrementing string key — so a UUID- or ULID-keyed model is documented correctly with no annotation. - An unbound segment is a required
string. - An optional segment (
{param?}) is documented as optional. - A route that resolves soft-deleted records (
->withTrashed()) notes it on the parameter, so consumers know a trashed record can resolve there.
A model binding also documents the implicit 404 the binding produces when the record is missing (one
404 per operation). See implicit responses.
Route::get('/invoices/{invoice}', [InvoiceController::class, 'show']); // {invoice} typed from Invoice's route keyReach for #[PathParameter] to refine a parameter — set
an OpenAPI format, a description, or an example, or give an unbound segment a non-string type:
#[PathParameter(name: 'invoice', type: 'string', format: 'uuid', description: 'Invoice public id')]public function show(Invoice $invoice): InvoiceResource { /* … */ }Query parameters
Section titled “Query parameters”Docuccino expresses validation by HTTP verb: read verbs (GET and HEAD) turn the recovered
rules into query parameters, and every other verb (POST, PUT, PATCH, DELETE) gets a request
body. So the same validation rules that document a create endpoint’s body document a list endpoint’s
query string.
// A GET action that validates its input documents query parameters:public function index(Request $request): AnonymousResourceCollection{ $request->validate([ 'status' => ['nullable', 'in:draft,sent,paid'], 'per_page' => ['nullable', 'integer', 'max:100'], ]); // …}Each field becomes one query parameter carrying that field’s schema; a rule that produced a
description (in: on an enum class, a required_if, a date format) puts it on the parameter’s
description rather than inside its schema.
List endpoints also get their filters, sorts, includes, and pagination inferred straight from
Spatie Query Builder — no annotation needed. To add or patch one
parameter by hand, reach for #[QueryParameter].
Request bodies
Section titled “Request bodies”A Form Request’s rules() — or an inline $request->validate([...]) / Validator::make(...) in the
action — becomes the request body schema.
public function rules(): array{ return [ 'customer_id' => ['required', 'integer'], 'currency' => ['required', 'in:GBP,USD,EUR'], 'due_at' => ['nullable', 'date'], ];}public function store(Request $request): InvoiceResource{ $request->validate([ 'customer_id' => ['required', 'integer'], 'currency' => ['required', 'in:GBP,USD,EUR'], 'due_at' => ['nullable', 'date'], ]); // …}// Form Request: the body hoists to a named component the operation $refs."requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StoreInvoiceRequest" } } }},// components.schemas"StoreInvoiceRequest": { "type": "object", "properties": { "customer_id": { "type": "integer" }, "currency": { "type": "string", "enum": ["GBP", "USD", "EUR"] }, "due_at": { "type": ["string", "null"], "format": "date" } }, "required": ["customer_id", "currency"]}Either way documents a body with customer_id, currency (as an enum), and due_at, with the right
required fields and formats. requestBody.required is true as soon as at least one field is
required, and omitted when every field is optional.
Because a Form Request is a single named source class, its body is hoisted to a
#/components/schemas/StoreInvoiceRequest component and the operation $refs it — so the same
request class used across several endpoints is one shared, deduped component. Rename the component
with #[SchemaName], and pin its diff identity across a
class rename with #[SchemaId].
An inline validate() / Validator::make() body has no source class to name honestly, so it
stays inline at the operation. To patch a single property of the inferred body — a description, an
example — use #[BodyParameter]; an operation carrying
one keeps its body inline for that operation, because the patch adjusts a property the shared
component must not carry.
Where the rules can come from
Section titled “Where the rules can come from”The rules array is recovered without ever being executed, from any of these:
| Source | How it’s found |
|---|---|
| A Form Request type-hinted on the action | Its rules() is analyzed as a constant array. |
$request->validate([...]) in the action |
Read from the action body. |
Validator::make($data, [...]) in the action |
The second argument is read. |
| Either of the above inside a helper the action calls | Docuccino descends into your own code (bounded), so a Validator::make(...) built in a query or service class one or more hops away is still found. Vendor code is never entered. |
Within a rules array, Docuccino reads pipe strings ('required|integer'), arrays of rules
(['required', 'integer']), the Rule:: factories Rule::enum(), Rule::in(), Rule::exists() and
Rule::unique(), and your own rule objects — new SortCode documents from the rule class’s
#[RuleSchema]. A field whose rules are a closure, a rule object with no
#[RuleSchema], or a conditional descriptor can’t be read statically: it’s omitted from the schema and
raises a validation.rule-unrecoverable info diagnostic naming the field, so nothing disappears
silently.
Nested and array fields
Section titled “Nested and array fields”Dot and wildcard paths build a real schema tree. A * segment turns the current node into an array
and descends into its items, so an items.*.sku rule documents items as an array of objects with a
sku property:
public function rules(): array{ return [ 'reference' => ['required', 'string', 'max:32'], 'items' => ['required', 'array', 'min:1'], 'items.*.sku' => ['required', 'string'], 'items.*.qty' => ['required', 'integer', 'min:1'], 'billing.email' => ['required', 'email'], ];}{ "type": "object", "properties": { "reference": { "type": "string", "maxLength": 32 }, "items": { "type": "array", "minItems": 1, "items": { "type": "object", "properties": { "sku": { "type": "string" }, "qty": { "type": "integer", "minimum": 1 } }, "required": ["sku", "qty"] } }, "billing": { "type": "object", "properties": { "email": { "type": "string", "format": "email" } }, "required": ["email"] } }, "required": ["reference", "items", "billing"]}Notice min:1 on items: size rules are applied type-aware, so the same rule bounds minItems
on an array, minimum on a number, and minLength on anything else. Rule order in your array
doesn’t matter — Docuccino sorts each field’s rules into Laravel’s own effect order first, so
['max:100', 'integer'] and ['integer', 'max:100'] produce identical output.
Documenting a custom rule
Section titled “Documenting a custom rule”A custom ValidationRule class does its work at runtime, so there’s nothing to read from its body. Say
what it accepts once, on the class, with #[RuleSchema] — and every field validated by it is documented,
in Form Requests, inline validate() calls, and Spatie Data’s #[Rule(new …)] alike:
use Docuccino\Attributes\RuleSchema;
#[RuleSchema( type: 'string', pattern: '[0-9]{2}-[0-9]{2}-[0-9]{2}', min: 8, max: 8, description: 'A UK sort code, hyphenated.', example: '20-15-55',)]final class SortCode implements ValidationRule{ public function validate(string $attribute, mixed $value, Closure $fail): void { /* … */ }}public function rules(): array{ return [ 'amount' => ['required', 'integer', 'min:1'], 'sort_code' => ['required', new SortCode], ];}{ "sort_code": { "type": "string", "pattern": "[0-9]{2}-[0-9]{2}-[0-9]{2}", "minLength": 8, "maxLength": 8, "description": "A UK sort code, hyphenated.", "example": "20-15-55" }}Every field is optional, and each one maps onto the same rule the vocabulary below already documents —
so a min bounds minLength, minimum or minItems by the resolved type, exactly like min:8 would:
| Field | Documented as |
|---|---|
type |
A JSON Schema type name (string, integer, number, boolean, array) — or any type rule name, so 'email' and 'uuid' bring their formats along. |
enum |
An enum of the allowed values, like in:…. |
pattern |
A bare ECMA-262 pattern, like regex:…. |
min / max |
The type-aware size keywords, like min: / max:. |
format |
A format, unless the type rule already implied one. |
description |
The property description (appended to any note the other rules produced). |
example |
The property example, typed to match the field. |
Validation rule reference
Section titled “Validation rule reference”This is the complete rule vocabulary. Anything not listed leaves the property permissive and raises a
validation.rule-unhandled info diagnostic — and you can teach Docuccino a new rule by registering a
RuleTransformer (see Writing an integration).
Presence
Section titled “Presence”| Rule | Documented as |
|---|---|
required, present |
The field joins the object’s required list. |
nullable |
The type admits null (type: [string, null]). |
sometimes |
Keeps the field out of required — even alongside required, because sometimes|required means “required when present”. |
filled |
Recognized; no effect on the documented shape. |
required_if, required_unless, required_with, required_with_all, required_without, required_without_all |
A description note (“Required when status is paid.”) — a conditional requirement has no JSON Schema keyword. |
Types and formats
Section titled “Types and formats”| Rule | Documented as |
|---|---|
string |
string |
integer, int |
integer |
numeric |
number |
boolean, bool |
boolean |
array |
array |
email |
string, format: email |
uuid |
string, format: uuid |
ulid |
string, format: ulid |
url |
string, format: uri |
ip |
string, format: ip |
date |
string, format: date |
date_format:Y-m-d H:i |
string, format: date-time when the pattern carries a time token, else date — plus “Expected format: …” so the exact contract survives. |
json |
string with contentMediaType: application/json — the value is a string carrying JSON. |
timezone |
string, described as “Must be a valid timezone identifier.” |
Sizes and ranges
Section titled “Sizes and ranges”| Rule | Documented as |
|---|---|
min:n, max:n |
minimum/maximum on a number, minItems/maxItems on an array, minLength/maxLength otherwise. |
between:a,b |
Both bounds, same type-aware keywords. |
size:n |
Both bounds pinned to n. |
gt:n, lt:n |
exclusiveMinimum / exclusiveMaximum (defaults the field to number). |
gte:n, lte:n |
minimum / maximum. |
gt:other_field (any comparison against a field) |
A description note — it’s a runtime relationship, not a bound. |
multiple_of:n |
multipleOf |
decimal:2, decimal:1,4 |
number, plus a note (“Must have 2 decimal places.” / “Must have between 1 and 4 decimal places.”) — JSON Schema has no decimal-places keyword. |
digits:8, digits_between:2,4, min_digits:n, max_digits:n |
An anchored digit pattern — ^\d{8}$, ^\d{2,4}$, ^\d{n,}$, ^\d{1,n}$. |
Choices and patterns
Section titled “Choices and patterns”| Rule | Documented as |
|---|---|
in:draft,sent,paid / Rule::in(...) / Rule::enum(InvoiceStatus::class) |
An enum of the allowed values — integer-typed when every value is a whole number, else string. Rule::enum() reads the enum’s real cases and names the class in the description; a ->only([...]) / ->except([...]) chain narrows the documented values to match. |
not_in:a,b |
not: { enum: ["a", "b"] } |
regex:/^INV-\d+$/ |
pattern, with the PHP delimiters and flags stripped so it’s a bare ECMA-262 regex. |
alpha, alpha_num, alpha_dash |
The equivalent anchored pattern. |
starts_with:INV- / ends_with:.pdf |
An anchored pattern for a single value; a multi-value set becomes a description (“Must start with one of: …”), since there’s no single pattern for it. |
accepted, accepted_if |
boolean with const: true (the _if form adds “Must be accepted when …”). |
declined, declined_if |
boolean with const: false. |
list |
array |
distinct |
uniqueItems: true |
exists, unique |
A type only (string if the field is otherwise untyped) — a foreign-key lookup isn’t a documentable shape. |
confirmed |
Adds the implicit {field}_confirmation partner, mirroring the field’s type and required flag. |
Schema annotations
Section titled “Schema annotations”| Rule | Documented as |
|---|---|
format:iban |
A format, unless a type rule already set one. |
description:… |
The property description, appended to any note earlier rules produced. |
example:… |
The property example, coerced to the field’s resolved type. |
These three aren’t Laravel rules, so don’t put them in a rules array — Laravel would reject them. They
exist so a #[RuleSchema] reaches the schema through the same chain as
every other rule, and they’re applied last, so they see the finished property.
Recognized with no schema effect
Section titled “Recognized with no schema effect”bail, exclude, exclude_if, exclude_unless, exclude_with, exclude_without,
current_password, prohibited, prohibits. These are consumed deliberately — they’re valid rules
that say nothing about the wire shape, so they never raise an unhandled-rule diagnostic.
File uploads
Section titled “File uploads”A file or image rule documents the field as type: string, format: binary; a file, image,
mimes, mimetypes, extensions, or dimensions rule also flips the request media type to
multipart/form-data automatically, with the rest of the body carried alongside it. An image rule
adds “An image file.” as the description.
mimes, mimetypes and extensions only mark the request as an upload — OpenAPI expresses an
accepted file type through the media type, not a schema keyword, so nothing is invented on the
property. dimensions likewise records its width/height constraints as a description note, since there
is no keyword for pixel dimensions.
public function rules(): array{ return [ 'invoice_id' => ['required', 'integer'], 'attachment' => ['required', 'file', 'mimes:pdf'], // → binary string, multipart/form-data ];}A file size bound (max, min, size, between) on a file field is in kilobytes, so it is
documented as a description note (e.g. “Maximum file size: 2048 KB.”) rather than a string-length
keyword, which would be wrong.
UploadedFile-typed Data properties
Section titled “UploadedFile-typed Data properties”You don’t always spell out a file rule. A Spatie Data property
typed Illuminate\Http\UploadedFile is a file upload by its type alone — so it’s documented as a
binary string and flips the body to multipart/form-data, even when the rules that constrain it are
computed at runtime and can’t be read statically. ?UploadedFile stays nullable, a list of
UploadedFile becomes a multipart array of binary items, and scalar properties alongside it are
carried in the same multipart body:
final class CreateUploadData extends Data{ public function __construct( public readonly UploadedFile $file, // → binary string, multipart/form-data public readonly string $collection = 'default', // carried alongside it ) {}}An explicit file/image rule or attribute is honored as before and never doubled.
Headers and cookies
Section titled “Headers and cookies”Headers and cookies aren’t inferable — nothing in a route or a rules array names them — so declare
them where they matter. #[HeaderParameter] and
#[CookieParameter] are repeatable and take the same
shape:
#[HeaderParameter(name: 'X-Idempotency-Key', type: 'string', required: true, description: 'Replay guard for retries')]#[CookieParameter(name: 'invoice_preview', type: 'string', description: 'Signed preview token')]public function store(StoreInvoiceRequest $request): InvoiceResource { /* … */ }To drop a parameter Docuccino inferred but you don’t want published, use
#[IgnoreParam] — optionally scoped to one location,
#[IgnoreParam(name: 'trace', in: 'query')].
When it can’t tell
Section titled “When it can’t tell”If a rule set can’t be resolved statically, Docuccino contributes what it can and leaves the rest to
your annotations, recording an info diagnostic rather than guessing. A
#[BodyParameter],
#[QueryParameter], or a docblock always takes
precedence over what’s inferred — you’re never fighting the tool.
Implicit 422
Section titled “Implicit 422”Because Docuccino recovers your request’s validation rules, it also documents the 422 validation
error that request produces — so a validated write endpoint carries its 422 automatically. A read
route’s rules become query parameters rather than a body, so it gets no implicit 422. See
implicit responses.