Skip to content

fix(literal): use Object.is so literal(NaN) accepts NaN - #1529

Open
spokodev wants to merge 5 commits into
open-circle:mainfrom
spokodev:fix/literal-nan-object-is
Open

fix(literal): use Object.is so literal(NaN) accepts NaN#1529
spokodev wants to merge 5 commits into
open-circle:mainfrom
spokodev:fix/literal-nan-object-is

Conversation

@spokodev

@spokodev spokodev commented Jun 30, 2026

Copy link
Copy Markdown

Problem

literal(NaN) is an unsatisfiable schema: it rejects every input, including the NaN it was constructed from.

import * as v from 'valibot';

v.parse(v.literal(NaN), NaN); // throws: Invalid type: Expected NaN but received NaN
v.is(v.literal(NaN), NaN);    // false

NaN is a valid number, and Literal = bigint | boolean | number | string | symbol, so literal(NaN) is a fully type-checked construction. The runtime can never succeed because the value check uses ===, and NaN === NaN is false.

The sibling picklist schema disagrees, because it compares with Array.includes (SameValueZero):

v.is(v.picklist([NaN]), NaN); // true

So literal and picklist give opposite answers for the same value, which points to literal's === being an oversight rather than a deliberate choice.

Fix

library/src/schemas/literal/literal.ts compares dataset.value === this.literal. Switch that single comparison to Object.is:

// before
if (dataset.value === this.literal) {
// after
if (Object.is(dataset.value, this.literal)) {

Object.is(NaN, NaN) is true, so literal(NaN) now accepts NaN.

This matches the equality migration already in flight: #1517 moves _merge (used by intersect) to Object.is, and #1477 moves value / notValue / values / notValues to Object.is. Neither of those touches the literal schema, so this closes the same NaN gap one layer up.

Trade-off

Like #1517 and #1477, Object.is also distinguishes -0 from +0. After this change literal(0) no longer accepts -0 (under === they were treated as equal). This is consistent with the direction of those PRs. If matching picklist's SameValueZero (which keeps -0/+0 loosely equal) is preferred instead, I can switch to that.

Tests

Added one case to library/src/schemas/literal/literal.test.ts:

test('for valid NaN literal', () => {
  expectNoSchemaIssue(literal(NaN), [NaN]);
});

It fails on main (literal(NaN) returns an issue) and passes with the fix. Full library/ suite stays green: 272 files / 3057 tests.

Summary by CodeRabbit

  • Bug Fixes
    • Improved literal schema matching for special numeric values: literal(NaN) now correctly matches datasets containing NaN, and signed zero is handled so -0 and +0 are treated as equivalent.
  • Tests
    • Added coverage to confirm datasets with NaN produce no schema issues when validating against a matching literal(NaN) schema.

The literal schema compares with === at the value check. Since
NaN === NaN is false, literal(NaN) was an unsatisfiable schema: it
rejected every input including the NaN it was built from. NaN is a
valid number and Literal includes number, so literal(NaN) is a
type-checked construction whose runtime could never succeed.

The sibling picklist schema accepts NaN (it uses Array.includes,
which is SameValueZero), so the two disagreed on NaN.

Switch the comparison to Object.is, matching the equality migration
in open-circle#1517 (_merge) and open-circle#1477 (value/notValue/values/notValues).
Object.is(NaN, NaN) is true, so literal(NaN) now accepts NaN. As a
side effect literal(0) no longer accepts -0 (=== treated them as
equal), consistent with the Object.is direction of open-circle#1517/open-circle#1477.
@dosubot dosubot Bot added size:XS This PR changes 0-9 lines, ignoring generated files. fix A smaller enhancement or bug fix labels Jun 30, 2026
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c047d95a-139b-43e3-9784-c2f10ab48951

📥 Commits

Reviewing files that changed from the base of the PR and between 4273438 and c5168a1.

📒 Files selected for processing (1)
  • library/src/schemas/literal/literal.ts
💤 Files with no reviewable changes (1)
  • library/src/schemas/literal/literal.ts

Walkthrough

The literal schema’s ~run equality check now matches NaN values while keeping the existing success and failure behavior. The test suite adds coverage for literal(NaN) and retains the signed zero checks for literal(0) and literal(-0).

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: updating literal comparison behavior so literal(NaN) accepts NaN.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@library/src/schemas/literal/literal.ts`:
- Around line 97-99: Update the `~run` implementation in `literal.ts` so it no
longer uses `Object.is` for all comparisons, since that changes zero-literal
behavior. Special-case `NaN` to use a NaN-safe check, and keep strict equality
(`===`) for every other `literal(...)` value so `literal(0)` still accepts `-0`
and `literal(-0)` still accepts `0`. Use the existing `literal` class and its
`~run(dataset, config)` method as the place to make the comparison logic change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3ae6e97b-36d9-48e7-85fe-fd693e8d34f1

📥 Commits

Reviewing files that changed from the base of the PR and between 0dc26ea and 32a0e08.

📒 Files selected for processing (2)
  • library/src/schemas/literal/literal.test.ts
  • library/src/schemas/literal/literal.ts

Comment thread library/src/schemas/literal/literal.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 2 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="library/src/schemas/literal/literal.ts">

<violation number="1" location="library/src/schemas/literal/literal.ts:98">
P2: Switching literal equality to Object.is introduces a signed-zero behavior change and inconsistency with picklist.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread library/src/schemas/literal/literal.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
library/src/schemas/literal/literal.ts (2)

102-102: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Stray blank line inside the condition.

Line 102 appears to be a whitespace-only line left in the middle of the boolean expression; remove it for readability.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@library/src/schemas/literal/literal.ts` at line 102, There is a stray
whitespace-only blank line inside the boolean condition in the literal schema
logic, which breaks readability. Remove the empty line from the condition in the
relevant expression in literal.ts, keeping the boolean check contiguous and
compact so the surrounding logic remains easy to scan.

100-104: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

NaN and signed-zero fix looks correct.

Using dataset.value === this.literal || (dataset.value !== dataset.value && this.literal !== this.literal) correctly matches NaN to NaN via the self-inequality idiom while keeping === for everything else, so -0/+0 still compare equal as before. This addresses the prior review comment requesting a NaN-specific special case instead of blanket Object.is.

The Biome noSelfCompare warning on line 103 is a false positive here since the self-comparison is the intentional NaN-detection idiom; consider adding a // biome-ignore lint/suspicious/noSelfCompare: NaN check comment to suppress it and document intent for future readers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@library/src/schemas/literal/literal.ts` around lines 100 - 104, The
NaN/signed-zero comparison in literal matching is correct, but the intentional
self-comparison triggers Biome’s noSelfCompare warning. In the `Literal`
schema’s equality check, add a targeted Biome ignore comment directly above the
`dataset.value !== dataset.value && this.literal !== this.literal` NaN guard to
document that the self-comparison is deliberate. Keep the existing `Literal`
logic unchanged and place the suppression close to the
`dataset.value`/`this.literal` comparison so future readers understand the
intent.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@library/src/schemas/literal/literal.ts`:
- Line 102: There is a stray whitespace-only blank line inside the boolean
condition in the literal schema logic, which breaks readability. Remove the
empty line from the condition in the relevant expression in literal.ts, keeping
the boolean check contiguous and compact so the surrounding logic remains easy
to scan.
- Around line 100-104: The NaN/signed-zero comparison in literal matching is
correct, but the intentional self-comparison triggers Biome’s noSelfCompare
warning. In the `Literal` schema’s equality check, add a targeted Biome ignore
comment directly above the `dataset.value !== dataset.value && this.literal !==
this.literal` NaN guard to document that the self-comparison is deliberate. Keep
the existing `Literal` logic unchanged and place the suppression close to the
`dataset.value`/`this.literal` comparison so future readers understand the
intent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 547d4157-3704-4529-9275-b4440b803b9f

📥 Commits

Reviewing files that changed from the base of the PR and between 32a0e08 and 4273438.

📒 Files selected for processing (2)
  • library/src/schemas/literal/literal.test.ts
  • library/src/schemas/literal/literal.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • library/src/schemas/literal/literal.test.ts

@pkg-pr-new

pkg-pr-new Bot commented Jul 21, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/valibot@1529

commit: c5168a1

@yslpn yslpn self-assigned this Jul 21, 2026
@spokodev

Copy link
Copy Markdown
Author

Quick clarification on the automated note above: this uses SameValueZero, not Object.is

dataset.value === this.literal ||
(dataset.value !== dataset.value && this.literal !== this.literal)

So -0 and +0 still compare as equal (no signed-zero behaviour change) and it stays consistent with picklist; the only new behaviour is NaN matching NaN, which is the fix. The flagged signed-zero concern therefore doesn't apply.

@yslpn

yslpn commented Jul 24, 2026

Copy link
Copy Markdown
Member

@spokodev Thanks for your input. Please use Prettier for your changes

I believe the fix is ​​correct; we'll discuss it soon wit Fabian

@spokodev

Copy link
Copy Markdown
Author

Ran Prettier — only a stray blank line in the condition changed. No rush on the review.

yslpn
yslpn previously approved these changes Jul 26, 2026
@dosubot dosubot Bot added the lgtm This PR has been approved by a maintainer label Jul 26, 2026
@yslpn
yslpn dismissed their stale review July 26, 2026 16:27

We will discuss different approaches

#1529
#1517
#1477

@yslpn yslpn removed the lgtm This PR has been approved by a maintainer label Jul 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix A smaller enhancement or bug fix size:XS This PR changes 0-9 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants