laravel/framework: Validation rules: false values passed to where() / whereNot() are no longer lost
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:
-
Serialized rule strings in your own tests or custom presence verifiers. If you assert on rule output or parse it yourself, expectations for
falsevalues move from""to"0"(and from"!"to"!0"). -
The one intentional behavior change: code that passed
falseto match empty strings in a string column no longer does that. For example,where('name', false)previously (accidentally) producedname = ''; it now producesname = 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
falsepassed towhere()/whereNot()now serializes as"0"instead of being lost as"".trueis 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
falseto match empty strings in a string column, pass''explicitly now. - Update any of your own tests that assert on the serialized rule string for
falsevalues.