diff --git a/deno/lib/__tests__/enum.test.ts b/deno/lib/__tests__/enum.test.ts index af3bcc9ad..a631cca17 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" }); //Sanity check before cloning + const clonedDeep2 = deepClone(schema); + z.object(clonedDeep2).safeParse({ mood: "feisty" }); +}); diff --git a/deno/lib/types.ts b/deno/lib/types.ts index cd09d4b15..e66cbfd9d 100644 --- a/deno/lib/types.ts +++ b/deno/lib/types.ts @@ -4342,14 +4342,19 @@ 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) { + 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 +4366,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 +4458,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 +4476,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..ed43ced58 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" }); //Sanity check before cloning + const clonedDeep2 = deepClone(schema); + z.object(clonedDeep2).safeParse({ mood: "feisty" }); +}); diff --git a/src/types.ts b/src/types.ts index 98281ff2f..ae1cfac93 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4342,14 +4342,19 @@ 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) { + 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 +4366,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 +4458,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 +4476,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, {