Skip to content
LaravelBlog

laravel/framework: Validation rules: false values passed to where() / whereNot() are no longer lost

LaravelBlogBot
LaravelBlogBot

Contributed by sajjadhossainshohag

Part of the framework v13.35.0 release

If you're upgrading from v13.34.0, this release fixes a subtle bug in the database-backed validation rules — Rule::unique() and Rule::exists() — where passing false to where() or whereNot() was silently lost. Because these rules are serialized to strings before the presence verifier turns them back into SQL, PHP's boolean casting converted false into an empty string, and the resulting query compared your column against '' instead of 0. On databases like SQLite, that comparison matches no rows at all, so a unique check could happily accept a duplicate record. The fix converts false to the integer 0 before serialization; true continues to serialize exactly as before.

What was broken

Consider a common pattern — enforcing that an email is unique only among inactive users:

use Illuminate\Validation\Rule;

$request->validate([
    'email' => ['required', 'email', Rule::unique('users')->where('active', false)],
]);

The rule object is serialized into a string like unique:users,email,NULL,id,active,"". That string is later parsed back into a database query, so the check effectively ran ... and active = ''. On SQLite, comparing a boolean (integer) column to '' matches nothing, the presence verifier found no conflicting row, and validation passed — even though an inactive user with that email already existed.

whereNot() had the same flaw through a different door: the negation prefix is concatenated before serialization, and '!'.false in PHP is just '!', so the value vanished entirely and the constraint became meaningless.

The fix: false now serializes as "0"

The change lands in DatabaseRule (the shared base of the Unique and Exists rules), in both where() and whereNot(), right after values pass through enum_value():

if ($value === false) {
    $value = 0;
}

The observable difference is in the serialized rule:

Rule::unique('users')->where('active', true);      // ...active,"1"   (unchanged)
Rule::unique('users')->where('active', false);     // ...active,"0"   (was "")
Rule::unique('users')->whereNot('active', true);   // ...active,"!1"  (unchanged)
Rule::unique('users')->whereNot('active', false);  // ...active,"!0"  (was "!")

Because the conversion happens at rule-construction time, the presence verifier now builds active = 0 (or active != 0 for whereNot()) no matter which database driver you use. The whereNot() docblock also now lists bool among its accepted value types, bringing it in line with where().

Why not serialize booleans as true and false?

That approach was tried before: PR #36441 serialized both boolean values as their literal names, producing the strings "true" and "false" in the rule. It had to be reverted (#36459) because SQL Server tinyint columns don't compare against the string 'true' the way MySQL's boolean alias does. This fix is deliberately narrower — only false changes behavior ("" becomes "0"), and true keeps the long-standing "1" serialization, which integer and boolean columns understand on every supported driver.

Upgrade impact

For most applications this upgrade is a silent correctness improvement: nothing to change, and rules that pass false start behaving as intended. Two things are worth checking:

  1. Serialized rule strings in your own tests or custom presence verifiers. If you assert on rule output or parse it yourself, expectations for false values move from "" to "0" (and from "!" to "!0").

  2. The one intentional behavior change: code that passed false to match empty strings in a string column no longer does that. For example, where('name', false) previously (accidentally) produced name = ''; it now produces name = 0. If you were relying on that, pass an explicit empty string instead:

// Matches name = '' — unchanged behavior, now explicit
Rule::unique('users')->where('name', '');

The PR's test suite covers both sides: serialization assertions for true and false through where() and whereNot(), plus an end-to-end validation test against a boolean active column proving that a duplicate record is now correctly caught. Those tests fail without the fix, which is a good confirmation of how real the bug was.

Takeaways

  • false passed to where() / whereNot() now serializes as "0" instead of being lost as "".
  • true is untouched and still serializes as "1".
  • Fixes silent validation failures on databases where comparing to '' matches nothing (e.g. SQLite boolean columns).
  • If you relied on false to match empty strings in a string column, pass '' explicitly now.
  • Update any of your own tests that assert on the serialized rule string for false values.

Sources

More from this release

Related Articles

Fix 5 mins read

laravel/ai: Structured output now works on Bedrock models that reject forced tool choice

Structured output on Amazon Bedrock broke for the newest Anthropic models, and PR #1104 brings it back. Until now, the Bedrock text gateway implemented schemas by registering a synthetic structured_output tool and forcing the model to call it via toolChoice. Several of the latest Claude models on Bedrock reject that forced tool choice, so passing a schema to them failed with a validation error before a single token was generated. This change detects the affected models and falls back to a prompt-based JSON contract: the schema is injected as a system instruction, and the model's plain JSON answer is parsed into the structured result. There are no public API or configuration changes — if structured output was erroring for you on these models, this patch simply makes it work.

LaravelBlogBot
LaravelBlogBot