Type the Input, Then Validate It Anyway
Tools have two descriptions of their input, and both are necessary because they address different audiences.
The schema tells the model what to send:
'options' => $schema->array()
->items($schema->object([
'label' => $schema->string()
->description('The answer itself, as the user would say it. Keep it to a few words.')
->required(),
'description' => $schema->string()
->description('One short line explaining the option. Optional, but it is what makes the card readable at a glance.'),
]))
->min(2)
->max(6)
->description('Two to six options you generated for this specific business. Order them most to least likely.')
->required(),
Note that every field carries prose, not just a type. ->description('Order them most to least likely') is not machine-checkable and is exactly the kind of instruction that improves output. The schema is prompt surface too — Chapter 3's point, arriving where it does the most work.
And then you validate anyway, because a schema is a request and not a guarantee:
$validated = $request->validate([
'question' => ['required', 'string', 'max:160'],
'field' => ['sometimes', 'string', 'in:services,hours,other'],
'options' => ['required', 'array', 'min:2', 'max:6'],
'options.*.label' => ['required', 'string', 'max:60'],
'multi' => ['sometimes', 'boolean'],
]);
Same rules, stated twice, in two languages. The schema is how you ask; the validator is how you insist. Skip the second and a well-formed-looking payload with seven options and a two-hundred-character label reaches your view layer.
Treat the boundary exactly like an HTTP boundary — because it is one. The caller is just unusual.
Flat Arguments Beat Nested Ones
This is the most portable lesson in the chapter.
A site can be a business or an event, and events need a start, an end and a timezone. The natural modelling is a nested object:
event: { start_at, end_at, timezone, venue: { address, phone } }
Models emit that unreliably. They drop a level, invert the nesting, or put venue at the top. So the tool takes it flat:
// Event-only. Taken flat and assembled into the nested `event` object below —
// flat arguments are far more reliable for a model to emit correctly than
// nested JSON.
'event_start_at' => ['required_if:category,event', 'date'],
'event_end_at' => ['required_if:category,event', 'date', 'after_or_equal:event_start_at'],
'event_timezone' => ['required_if:category,event', 'string', 'timezone'],
'event_venue_address' => ['sometimes', 'string', 'max:500'],
…and assembles the nested shape server-side, where it is a pure function of validated input and cannot go wrong.
The rule: make the model's job as shallow as possible, and do the structural work yourself. A flat list of well-named scalars with clear descriptions is something a model gets right nearly every time. Anything deeper is a reliability tax you are choosing to pay.
required_if:category,event is the other half. Conditional requirements let one tool serve both shapes without splitting into two tools that would then both need describing, ordering and maintaining.