fix: handle class instances with private fields#5140
Conversation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: ef5a13c The changes in this PR will be included in the next version bump. Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
✅ Deploy Preview for vee-validate-docs canceled.
|
✅ Deploy Preview for vee-validate-v5 ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
Pull request overview
This PR fixes issue #4977 where class instances with private fields (# syntax) would cause errors (TypeError: attempted to get private field on non-instance) when used as form values in vee-validate. The root cause is that klona (the deep-cloning library) cannot handle private fields, and Vue's reactivity system similarly triggers this error.
Changes:
- Added a
deepCopywrapper aroundklonainutils/common.tsthat detects non-plain objects (class instances) and returns them by reference, preventing cloning failures on private fields - Updated
isEqualinutils/assertions.tsto use reference equality for non-plain objects, avoiding property enumeration that could fail on class instances with private fields - Replaced all direct
klonaimports across five source files (validate.ts,useForm.ts,useFieldArray.ts,useField.ts,Form.ts) with the new safedeepCopywrapper
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
packages/vee-validate/src/utils/common.ts |
Core fix: adds deepCopy wrapper and isClassInstance helper |
packages/vee-validate/src/utils/assertions.ts |
Adds isPlainObject guard in isEqual for reference equality on class instances |
packages/vee-validate/src/validate.ts |
Replaces klona as deepCopy import with deepCopy from utils |
packages/vee-validate/src/useForm.ts |
Replaces klona as deepCopy import with deepCopy from utils |
packages/vee-validate/src/useFieldArray.ts |
Replaces klona as deepCopy import with deepCopy from utils |
packages/vee-validate/src/useField.ts |
Replaces klona as deepCopy import with deepCopy from utils |
packages/vee-validate/src/Form.ts |
Replaces klona as deepCopy import with deepCopy from utils |
packages/vee-validate/tests/utils/assertions.spec.ts |
Adds unit tests for new isEqual and deepCopy behavior with class instances |
.changeset/fix-4977-private-properties.md |
Changeset entry for the patch release |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Non-plain objects with [object Object] tag are class instances. | ||
| // They may have private fields that cannot be cloned, so return by reference. | ||
| if (isClassInstance(value)) { | ||
| return value; | ||
| } | ||
|
|
||
| // For arrays, recursively process each item to protect nested class instances | ||
| if (Array.isArray(value)) { | ||
| return value.map(item => deepCopy(item)) as T; | ||
| } | ||
|
|
||
| // For other known types that klona handles (Date, RegExp, Map, Set, etc.), delegate to klona | ||
| const tag = Object.prototype.toString.call(value); | ||
| if (tag !== '[object Object]') { | ||
| return klona(value); |
There was a problem hiding this comment.
The deepCopy function correctly handles class instances with [object Object] toString tag by detecting them via isClassInstance and returning by reference. However, class instances that define a custom Symbol.toStringTag (e.g., get [Symbol.toStringTag]() { return 'MyClass'; }) will have a tag different from [object Object], so isClassInstance returns false for them. They then fall through to klona(value) at line 46, which will still fail if those class instances contain private fields.
While this is an edge case, the inconsistency between deepCopy and isEqual is worth noting: isEqual uses isPlainObject for its guard, which does handle objects with custom Symbol.toStringTag correctly (returns false for them), so isEqual would use reference equality for these instances. But deepCopy would still attempt to clone them via klona, potentially throwing on private fields.
Consider using !isPlainObject(value) as the condition to return by reference in deepCopy as well (for non-array, non-null objects), rather than checking the tag separately, to ensure consistency with how isEqual handles these objects.
| // Non-plain objects with [object Object] tag are class instances. | |
| // They may have private fields that cannot be cloned, so return by reference. | |
| if (isClassInstance(value)) { | |
| return value; | |
| } | |
| // For arrays, recursively process each item to protect nested class instances | |
| if (Array.isArray(value)) { | |
| return value.map(item => deepCopy(item)) as T; | |
| } | |
| // For other known types that klona handles (Date, RegExp, Map, Set, etc.), delegate to klona | |
| const tag = Object.prototype.toString.call(value); | |
| if (tag !== '[object Object]') { | |
| return klona(value); | |
| // For arrays, recursively process each item to protect nested class instances | |
| if (Array.isArray(value)) { | |
| return value.map(item => deepCopy(item)) as T; | |
| } | |
| // Non-plain objects (including class instances and other built-ins) are returned by reference. | |
| // This avoids attempting to clone instances that may contain private fields and aligns with isEqual, | |
| // which also treats non-plain objects by reference. | |
| if (!isPlainObject(value)) { | |
| return value; |
Summary
#syntax) now work correctly as form valuesdeepCopywrapper aroundklonathat detects non-plain objects (class instances) and returns them by reference instead of attempting deep cloning, which would fail on private fieldsisEqualto use reference equality for class instances instead of trying to traverse their propertiesklonaimports across the codebase replaced with the safedeepCopywrapperTest plan
isEqualdoes not throw on class instances with private propertiesisEqualhandles class instances nested in plain objectsdeepCopydoes not throw on class instances with private propertiesdeepCopyhandles class instances nested in arraysdeepCopystill correctly clones plain objects and primitives🤖 Generated with Claude Code