Skip to content

fix: preserve invalid Date instead of dropping it to null#355

Open
greymoth-jp wants to merge 1 commit into
flightcontrolhq:mainfrom
greymoth-jp:fix-invalid-date-serialization
Open

fix: preserve invalid Date instead of dropping it to null#355
greymoth-jp wants to merge 1 commit into
flightcontrolhq:mainfrom
greymoth-jp:fix-invalid-date-serialization

Conversation

@greymoth-jp

@greymoth-jp greymoth-jp commented Jun 29, 2026

Copy link
Copy Markdown

Problem

isDate only matches dates with a valid time (payload instanceof Date && !isNaN(payload.valueOf())), so an invalid Date never reaches the Date transformer. It falls through to plain JSON, where JSON.stringify(new Date(NaN)) produces null. After parse, the value comes back as null, losing both the value and the Date type:

const d = new Date(NaN);            // also: new Date('not a date')
SuperJSON.parse(SuperJSON.stringify(d)); // => null, expected an invalid Date

This is a silent type change. Invalid Dates turn up in real code whenever a date is built from untrusted input, e.g. new Date(userInput) where the input does not parse. The caller then receives null where it expected a Date, which breaks any later .getTime() / .toISOString() call.

Fix

Match any Date instance in the Date rule and move the "no ISO representation" concern into the serializer: an invalid Date is serialized to an empty string, and new Date('') revives it back into an invalid Date. This is the same approach devalue uses (it stores an invalid Date as [["Date",""]] and reconstructs it with new Date('')).

isDate is left strict, so its existing meaning ("a valid Date") and its test stay untouched. Valid dates serialize exactly as before.

Verification

Checked both ways, with the existing suite plus a new case:

  • invalid Date serializes to { json: "", meta: { values: ["Date"] } } and parses back to an invalid Date (instanceof Date, NaN time)
  • valid Date: unchanged
  • null: stays null
  • nested invalid Dates inside objects/arrays: preserved

npm test stays green. The added "works for invalid dates" case fails on main and passes with this change.

Scope

Only the invalid-Date path changes. Output produced by older versions (where an invalid Date became null with no annotation) still deserializes to null, so there is no change for previously serialized data.

Greptile Summary

This PR fixes a silent type-erasure bug where invalid Date objects (e.g. new Date(NaN)) were serialized to null and lost their Date annotation on round-trip. The fix widens the isApplicable guard from the strict isDate helper to v instanceof Date and serializes invalid dates as an empty string "", which new Date("") faithfully revives to an invalid Date.

  • src/transformer.ts: replaces the isDate import with an inline v instanceof Date predicate and adds a ternary in the serializer that emits "" for invalid dates instead of calling .toISOString().
  • src/index.test.ts: adds a "works for invalid dates" case that verifies the serialized form ({ date: "" }), the Date annotation, and that the parsed value is instanceof Date with a NaN time.

Confidence Score: 5/5

Safe to merge — the only changed path is the invalid-Date serialization, and valid dates, nulls, and all other types are completely unaffected.

The fix is narrowly scoped: it adds one ternary in the Date serializer and widens the isApplicable guard from valid-only to any Date instance. Valid dates continue to emit their ISO string exactly as before. The empty-string sentinel for invalid dates is a well-established pattern (used by devalue), new Date('') is universally an Invalid Date, and backward compatibility with previously serialized data is preserved. Tests cover the new case, the existing valid-date case is unchanged, and no other logic in the file is touched.

No files require special attention.

Important Files Changed

Filename Overview
src/transformer.ts Replaces isDate guard with v instanceof Date and serializes invalid dates to "" instead of falling through to JSON null; logic is correct and backward-compatible.
src/index.test.ts Adds a "works for invalid dates" test case with correct assertions for the serialized form, the Date annotation, and the revived invalid Date.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["Value to serialize"] --> B{"v instanceof Date?"}
    B -- No --> C["Check other rules / fall through to JSON"]
    B -- Yes --> D{"isNaN(v.valueOf())?"}
    D -- Yes (invalid Date) --> E["Serialize to ''\n+ 'Date' annotation"]
    D -- No (valid Date) --> F["Serialize to v.toISOString()\n+ 'Date' annotation"]
    E --> G["JSON: { json: '', meta: { values: ['Date'] } }"]
    F --> H["JSON: { json: '2024-01-01T...', meta: { values: ['Date'] } }"]
    G --> I["Deserialize: new Date('')"]
    H --> J["Deserialize: new Date(isoString)"]
    I --> K["Invalid Date (NaN time)"]
    J --> L["Valid Date"]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["Value to serialize"] --> B{"v instanceof Date?"}
    B -- No --> C["Check other rules / fall through to JSON"]
    B -- Yes --> D{"isNaN(v.valueOf())?"}
    D -- Yes (invalid Date) --> E["Serialize to ''\n+ 'Date' annotation"]
    D -- No (valid Date) --> F["Serialize to v.toISOString()\n+ 'Date' annotation"]
    E --> G["JSON: { json: '', meta: { values: ['Date'] } }"]
    F --> H["JSON: { json: '2024-01-01T...', meta: { values: ['Date'] } }"]
    G --> I["Deserialize: new Date('')"]
    H --> J["Deserialize: new Date(isoString)"]
    I --> K["Invalid Date (NaN time)"]
    J --> L["Valid Date"]
Loading

Reviews (1): Last reviewed commit: "fix: preserve invalid Date instead of dr..." | Re-trigger Greptile

An invalid Date (e.g. new Date(NaN), or new Date('') from a failed parse)
was not matched by the Date transformer because isDate excludes dates with
a NaN time. It therefore fell through to plain JSON, where it became null,
losing both its value and its Date type on parse.

Match any Date instance in the Date rule and serialize an invalid Date to
an empty string, which new Date('') revives back into an invalid Date. Valid
dates are unchanged. isDate stays strict, so its existing semantics and test
are untouched.
@greymoth-jp greymoth-jp requested a review from Skn0tt as a code owner June 29, 2026 14:11

@Skn0tt Skn0tt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I like the idea! Left a note about the string representation. Let's make sure that this is backwards compatible, can you add a test case about that?

Comment thread src/index.test.ts
date: new Date('invalid'),
},
output: {
date: '',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

let's use Invalid Date as the sentinel value instead of ''.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants