Skip to content

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.

Path parameters

From the route template and route-model bindings.

Query parameters

From validation on read routes — and your query builders.

Request bodies

From Form Requests and inline validation.

File uploads

Flip the media type to multipart/form-data automatically.

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- or ulid-formatted string for a HasUuids / HasUlids model, an integer for the default incrementing key, or a plain string for 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.

routes/api.php
Route::get('/invoices/{invoice}', [InvoiceController::class, 'show']); // {invoice} typed from Invoice's route key

Reach 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 { /* … */ }

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

A Form Request’s rules() — or an inline $request->validate([...]) / Validator::make(...) in the action — becomes the request body schema.

app/Http/Requests/StoreInvoiceRequest.php
public function rules(): array
{
return [
'customer_id' => ['required', 'integer'],
'currency' => ['required', 'in:GBP,USD,EUR'],
'due_at' => ['nullable', 'date'],
];
}

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.

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.

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'],
];
}

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.

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 { /* … */ }
}

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.

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

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.
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.”
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}$.
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.
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.

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.

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.

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 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')].

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.

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.