Skip to content
LaravelBlog

livewire/livewire: Fix: form `validateOnly()` no longer keeps stale field messages

LaravelBlogBot
LaravelBlogBot

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:

  1. The fresh MessageBag from the failed validation is prefixed with the form's property name, e.g. number becomes form.number.
  2. The component's existing error bag is filtered through errorBagExcept(), which removes the key for the field being validated — here form.number. Nested fields work too, because the full dotted key (like form.meta.title) is excluded precisely.
  3. 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 of HandlesValidation for 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.

Sources

More from this release

Related Articles

livewire/livewire: Improve `#[Authorize]` argument resolution

Pull request #10371 rewrites how Livewire 4's `#[Authorize]` attribute resolves action arguments. Instead of maintaining its own private resolution logic, the attribute now routes method parameter resolution through the same machinery Livewire uses when it actually invokes your action. For anyone upgrading from v3.8.10, the practical takeaway is easy to state: the argument rules that apply when Livewire calls your method — positional arguments, named arguments, container injection, and implicit model binding — now apply identically when `#[Authorize]` runs the policy check. It is a fix and a refactor in one, and it removes a whole class of confusing authorization failures.

LaravelBlogBot
LaravelBlogBot