Skip to content

fix(serialize): read the constructor from the prototype - #207

Open
MFA-G wants to merge 1 commit into
unjs:mainfrom
MFA-G:fix/own-constructor-key
Open

MFA-G wants to merge 1 commit into
unjs:mainfrom
MFA-G:fix/own-constructor-key

Conversation

@MFA-G

@MFA-G MFA-G commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Problem

serializeObject reads the object's class with object.constructor, which resolves an own constructor key before the prototype one. Such a key is easy to produce — JSON.parse output, a parsed query object, any user supplied record — and it currently breaks serialization three different ways:

import { serialize, hash } from "ohash";

serialize({ constructor: null });
// TypeError: Cannot read properties of null (reading 'name')

serialize({ constructor: 1 });
// "undefined{constructor:1}"   <- stray `undefined` prefix

serialize({ constructor: Date });
// Date(...) handler is selected for a plain object

The first case is the worst one: hash(JSON.parse(userJson)) throws instead of returning a hash, so a cache key or an ETag computed from untrusted JSON can take down the request. The third is subtler — globalThis[objName] === constructor passes because the own value is the global, so a plain object is routed through $Date / $Map / ... and either throws or hashes as something it is not.

Fix

Read the constructor from the prototype instead of from the object:

const constructor = Object.getPrototypeOf(object)?.constructor;

Class detection is unchanged for real instances (new Test() still has Test on its prototype), and an own constructor key is now treated as ordinary data:

serialize({ constructor: null }); // "{constructor:null}"
serialize({ constructor: 1 });    // "{constructor:1}"

?. covers Object.create(null), which previously landed on the constructor === undefined branch and still resolves to "".

Bundle size

The hash gzip budget needed +10 bytes (1548 → 1559) for the Object.getPrototypeOf call, so the assertion moves from 1550 to 1560. The serialize and diff budgets are unchanged. Happy to drop the budget change and shrink this differently if you would rather hold the line at 1.55 kB.

Tests

New own \constructor` keycase intest/serialize.test.tscovering the throw, theundefinedprefix,JSON.parseinput, the global-collision path, and that distinct values still hash distinctly. Full suite green: 91 passed, pluslintandtest:types`.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed serialization for objects with their own constructor property, including data parsed from JSON.
    • Such properties are now preserved as regular data instead of causing errors, incorrect type handling, or misleading class-based output.
    • Serialization now consistently distinguishes objects with different constructor values.

An own `constructor` key shadowed the class lookup in `serializeObject`.
`{ constructor: null }` threw on `.name`, `{ constructor: 1 }` produced an
`undefined` prefix, and a value equal to a global (`{ constructor: Date }`)
selected that type's handler.

Such a key is trivial to produce — `JSON.parse` output, a query object, any
user supplied record — so reading it as the object's class is never correct.
Reading the constructor from the prototype keeps the class detection intact
for real class instances while treating an own key as plain data.

The gzipped `hash` bundle budget goes from 1550 to 1560 bytes (1548 -> 1559)
for the `Object.getPrototypeOf` call.
@coderabbitai

coderabbitai Bot commented Sep 14, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 6f21cb63-19ae-421e-9e36-66ad0d94077e

📥 Commits

Reviewing files that changed from the base of the PR and between 764b0a3 and fcc5add.

📒 Files selected for processing (3)
  • src/serialize.ts
  • test/bundle.test.ts
  • test/serialize.test.ts

Included review availability: Your plan provides up to 4 included reviews per hour; 3 remain after this review.


📝 Walkthrough

Walkthrough

Object serialization now reads the constructor from the prototype chain. Tests cover own constructor properties, global handlers, distinct hashes, and the updated gzip-size threshold.

Changes

Constructor serialization

Layer / File(s) Summary
Prototype constructor lookup and validation
src/serialize.ts, test/serialize.test.ts, test/bundle.test.ts
serializeObject uses the prototype constructor instead of an own constructor property. Tests cover null, primitive, parsed, global, and distinct constructor values. The gzip-size limit increases from 1550 to 1560 bytes.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Low

Suggested reviewers: pi0

Merge Risk: ⚪ Minimal · up to fcc5a

The serialization fix addresses own constructor-property handling without an identified remaining correctness or availability risk.

🚥 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 and concisely describes the main change: reading the constructor from the object's prototype during serialization.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 3…
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@codecov

codecov Bot commented Sep 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.66%. Comparing base (c5173ba) to head (fcc5add).
⚠️ Report is 112 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff             @@
##             main     #207       +/-   ##
===========================================
+ Coverage   80.31%   95.66%   +15.34%     
===========================================
  Files           8        6        -2     
  Lines        1006      369      -637     
  Branches      111       94       -17     
===========================================
- Hits          808      353      -455     
+ Misses        198       13      -185     
- Partials        0        3        +3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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.

1 participant