livewire/livewire: Fix: form `validateOnly()` no longer keeps stale field messages
Contributed by billyonecan
Part of the livewire v4.4.7 release
Livewire's form objects let you move validation rules and logic out of your component and into a dedicated class, but a subtle bug in validateOnly() meant that error messages from previous requests could stick around and be reported again for the field you're validating right now. Pull request #10375 fixes this: when a form object validates a single field, that field's messages are now replaced rather than merged on top of history. The fix ships on the 4.x branch, so if you're upgrading from v3.8.10 there is nothing to change in your code — you simply stop seeing duplicate and stale validation messages from form objects.
What changed
The entire fix is a one-line change in src/Features/SupportFormObjects/Form.php. When validateOnly() throws a ValidationException, Livewire prefixes the fresh error messages (so number becomes form.number) and merges them with the component's existing error bag. Previously, that merge pulled in the entire old bag:
// Before
$this->getComponent()->getErrorBag()
// After
$this->getComponent()->errorBagExcept($this->getPropertyName() . '.' . $field)
Now the key for the field currently being validated is excluded from the old bag before merging. This mirrors what Livewire\Features\SupportValidation\HandlesValidation already does for plain component properties, so form objects finally behave the same way as the rest of Livewire's validation layer.
The bug in practice
Consider a form object with two rules on the same field, and a component that validates it live:
use Livewire\Attributes\Validate;
use Livewire\Component;
use Livewire\Form;
class PostForm extends Form
{
#[Validate(['required', 'integer'])]
public $number = 1;
}
class PostEditor extends Component
{
public PostForm $form;
public function updated(string $property): void
{
$this->form->validateOnly($property);
}
}
If you emptied the field, $this->form->validateOnly('number') failed with The number field is required. Then, if you typed a into the same field, the next request failed again — but the response now contained both messages:
The number field is required.
The number field must be an integer.
The first message is stale: the field isn't empty anymore, so required no longer applies. The regression test added in this PR fails before the fix with exactly this symptom — a duplicated The title field is required. assertion — and passes after the change.
Why it matters
Three reasons. First, duplicates and stale messages are simply wrong output; a user who sees "required" while typing valid input quickly loses trust in the form. Second, the bug made form objects inconsistent with the rest of Livewire: calling validateOnly() on a component property (through HandlesValidation) already replaced messages for the field being validated — only form objects kept history. Third, real-time validation is where this hurts most, because validateOnly() typically fires on every keystroke or blur, so the error bag accumulates messages rapidly and the errors your front-end receives become harder to reason about.
How the fix works under the hood
When validation of a form field fails, the flow is now:
- The fresh
MessageBagfrom the failed validation is prefixed with the form's property name, e.g.numberbecomesform.number. - The component's existing error bag is filtered through
errorBagExcept(), which removes the key for the field being validated — hereform.number. Nested fields work too, because the full dotted key (likeform.meta.title) is excluded precisely. - The two bags are merged. Since the old entry was removed in step 2, the merge replaces the current field's messages instead of appending to them.
The failedRules array still receives the same prefix treatment as before, so rule introspection is unchanged.
Upgrade impact
- No API changes.
validateOnly(),getErrorBag(),errorBagExcept(), and the form object API are all untouched. - Behavioral change:
errors()->get('form.number')now returns only the latest message for the field you last validated, instead of accumulating one message per failed rule from past requests. - If you somehow depended on the old accumulate-and-merge behavior (unlikely, since it produced duplicates), review any test assertions that expect multiple messages under a single key.
- A regression test,
test_form_object_validate_only_replaces_messages_for_the_field_being_validated, now guards this behavior, and the PR closes issue #10372.
Takeaways
- Form object
validateOnly()no longer merges stale messages for the field being validated; messages are replaced. - The fix filters the component's error bag with
errorBagExcept(), matching the behavior ofHandlesValidationfor regular properties. - Upgrading from v3.8.10 requires no code changes — expect cleaner, duplicate-free validation output out of the box.
- Nested fields are handled correctly because exclusion uses the full prefixed key, like
form.meta.title.