From 24599b8f0c4f7ebf2ed5a06406adc7f9c9737e77 Mon Sep 17 00:00:00 2001 From: Alon Weiss Date: Fri, 27 Dec 2024 23:19:53 +0200 Subject: [PATCH] fix: replace es2019 class fields with local members to address #3611 --- deno/lib/__tests__/enum.test.ts | 59 +++++++++++++++++++++++++++++++++ deno/lib/types.ts | 25 +++++++------- src/__tests__/enum.test.ts | 59 +++++++++++++++++++++++++++++++++ src/types.ts | 25 +++++++------- 4 files changed, 144 insertions(+), 24 deletions(-) diff --git a/deno/lib/__tests__/enum.test.ts b/deno/lib/__tests__/enum.test.ts index af3bcc9ad..1ff982ded 100644 --- a/deno/lib/__tests__/enum.test.ts +++ b/deno/lib/__tests__/enum.test.ts @@ -88,3 +88,62 @@ test("readonly in ZodEnumDef", () => { let _t!: z.ZodEnumDef; _t; }); + +test("enum parsing works after cloning", () => { + function deepClone(value: any) { + // Handle null and undefined + if (value == null) { + return value; + } + + // Get the constructor and prototype + const constructor = Object.getPrototypeOf(value).constructor; + + // Handle primitive wrappers + if ([Boolean, Number, String].includes(constructor)) { + return new constructor(value); + } + + // Handle Date objects + if (constructor === Date) { + return new Date(value.getTime()); + } + + // Handle Arrays + if (constructor === Array) { + return value.map((item: any) => deepClone(item)); + } + + // Handle basic RegExp + if (constructor === RegExp) { + return new RegExp(value.source, value.flags); + } + + // Handle Objects (including custom classes) + if (typeof value === 'object') { + // Create new instance while preserving the prototype chain + const cloned = Object.create(Object.getPrototypeOf(value)); + + // Clone own properties + const descriptors = Object.getOwnPropertyDescriptors(value); + for (const [key, descriptor] of Object.entries(descriptors)) { + if (descriptor.value !== undefined) { + descriptor.value = deepClone(descriptor.value); + } + Object.defineProperty(cloned, key, descriptor); + } + + return cloned; + } + + // Return primitives and functions as is + return value; + } + + const schema = { + mood: z.enum(["happy", "sad", "neutral", "feisty"]), + }; + z.object(schema).safeParse({ mood: "feisty" }); // <-- This Works + const clonedDeep2 = deepClone(schema); + z.object(clonedDeep2).safeParse({ mood: "feisty" }); // <-- This Breaks +}); diff --git a/deno/lib/types.ts b/deno/lib/types.ts index cd09d4b15..3d43fa15d 100644 --- a/deno/lib/types.ts +++ b/deno/lib/types.ts @@ -4342,14 +4342,20 @@ function createZodEnum( }); } +const lookupSymbol = Symbol("lookup"); export class ZodEnum extends ZodType< T[number], ZodEnumDef, T[number] > { - #cache: Set | undefined; - + private [lookupSymbol]: Set | undefined; _parse(input: ParseInput): ParseReturnType { + let lookup = this[lookupSymbol]; + if (!lookup) { + console.log("setting lookup"); + lookup = new Set(this._def.values); + this[lookupSymbol] = lookup; + } if (typeof input.data !== "string") { const ctx = this._getOrReturnCtx(input); const expectedValues = this._def.values; @@ -4361,14 +4367,9 @@ export class ZodEnum extends ZodType< return INVALID; } - if (!this.#cache) { - this.#cache = new Set(this._def.values); - } - - if (!this.#cache.has(input.data)) { + if (!lookup.has(input.data)) { const ctx = this._getOrReturnCtx(input); const expectedValues = this._def.values; - addIssueToContext(ctx, { received: ctx.data, code: ZodIssueCode.invalid_enum_value, @@ -4458,7 +4459,7 @@ export class ZodNativeEnum extends ZodType< ZodNativeEnumDef, T[keyof T] > { - #cache: Set | undefined; + private [lookupSymbol]: Set | undefined; _parse(input: ParseInput): ParseReturnType { const nativeEnumValues = util.getValidEnumValues(this._def.values); @@ -4476,11 +4477,11 @@ export class ZodNativeEnum extends ZodType< return INVALID; } - if (!this.#cache) { - this.#cache = new Set(util.getValidEnumValues(this._def.values)); + if (!this[lookupSymbol]) { + this[lookupSymbol] = new Set(util.getValidEnumValues(this._def.values)); } - if (!this.#cache.has(input.data)) { + if (!this[lookupSymbol].has(input.data)) { const expectedValues = util.objectValues(nativeEnumValues); addIssueToContext(ctx, { diff --git a/src/__tests__/enum.test.ts b/src/__tests__/enum.test.ts index 75c79abfd..6e275510f 100644 --- a/src/__tests__/enum.test.ts +++ b/src/__tests__/enum.test.ts @@ -87,3 +87,62 @@ test("readonly in ZodEnumDef", () => { let _t!: z.ZodEnumDef; _t; }); + +test("enum parsing works after cloning", () => { + function deepClone(value: any) { + // Handle null and undefined + if (value == null) { + return value; + } + + // Get the constructor and prototype + const constructor = Object.getPrototypeOf(value).constructor; + + // Handle primitive wrappers + if ([Boolean, Number, String].includes(constructor)) { + return new constructor(value); + } + + // Handle Date objects + if (constructor === Date) { + return new Date(value.getTime()); + } + + // Handle Arrays + if (constructor === Array) { + return value.map((item: any) => deepClone(item)); + } + + // Handle basic RegExp + if (constructor === RegExp) { + return new RegExp(value.source, value.flags); + } + + // Handle Objects (including custom classes) + if (typeof value === 'object') { + // Create new instance while preserving the prototype chain + const cloned = Object.create(Object.getPrototypeOf(value)); + + // Clone own properties + const descriptors = Object.getOwnPropertyDescriptors(value); + for (const [key, descriptor] of Object.entries(descriptors)) { + if (descriptor.value !== undefined) { + descriptor.value = deepClone(descriptor.value); + } + Object.defineProperty(cloned, key, descriptor); + } + + return cloned; + } + + // Return primitives and functions as is + return value; + } + + const schema = { + mood: z.enum(["happy", "sad", "neutral", "feisty"]), + }; + z.object(schema).safeParse({ mood: "feisty" }); // <-- This Works + const clonedDeep2 = deepClone(schema); + z.object(clonedDeep2).safeParse({ mood: "feisty" }); // <-- This Breaks +}); diff --git a/src/types.ts b/src/types.ts index 98281ff2f..88b7255dd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4342,14 +4342,20 @@ function createZodEnum( }); } +const lookupSymbol = Symbol("lookup"); export class ZodEnum extends ZodType< T[number], ZodEnumDef, T[number] > { - #cache: Set | undefined; - + private [lookupSymbol]: Set | undefined; _parse(input: ParseInput): ParseReturnType { + let lookup = this[lookupSymbol]; + if (!lookup) { + console.log("setting lookup"); + lookup = new Set(this._def.values); + this[lookupSymbol] = lookup; + } if (typeof input.data !== "string") { const ctx = this._getOrReturnCtx(input); const expectedValues = this._def.values; @@ -4361,14 +4367,9 @@ export class ZodEnum extends ZodType< return INVALID; } - if (!this.#cache) { - this.#cache = new Set(this._def.values); - } - - if (!this.#cache.has(input.data)) { + if (!lookup.has(input.data)) { const ctx = this._getOrReturnCtx(input); const expectedValues = this._def.values; - addIssueToContext(ctx, { received: ctx.data, code: ZodIssueCode.invalid_enum_value, @@ -4458,7 +4459,7 @@ export class ZodNativeEnum extends ZodType< ZodNativeEnumDef, T[keyof T] > { - #cache: Set | undefined; + private [lookupSymbol]: Set | undefined; _parse(input: ParseInput): ParseReturnType { const nativeEnumValues = util.getValidEnumValues(this._def.values); @@ -4476,11 +4477,11 @@ export class ZodNativeEnum extends ZodType< return INVALID; } - if (!this.#cache) { - this.#cache = new Set(util.getValidEnumValues(this._def.values)); + if (!this[lookupSymbol]) { + this[lookupSymbol] = new Set(util.getValidEnumValues(this._def.values)); } - if (!this.#cache.has(input.data)) { + if (!this[lookupSymbol].has(input.data)) { const expectedValues = util.objectValues(nativeEnumValues); addIssueToContext(ctx, {