zod icon indicating copy to clipboard operation
zod copied to clipboard

Bug: .merge changes schema to disallow extra props when importing commonJs build

Open bvandercar-vt opened this issue 1 year ago • 5 comments

The following error occurs when the parser encounters an unexpected property: Expected never, received <type - array/string/etc.>. In other words, it's as if it has .strict(), even though that was never applied.

This happens in the following scenario:

  • importing a schema from a commonJs file AND
  • performing a .merge with that schema THEN
  • with the new schema, parsing an object that has extra (unexpected) properties.

This issue can best be explained / understood by looking at the CodeSandbox. The CodeSandbox runs the tests to show which case fails. CodeSandbox: https://codesandbox.io/p/devbox/zod-vite-bug-y2qsc8?file=%2Fsrc%2Ftest.spec.ts

CodeSandbox test file:

// baseSchema is a basic `z.object({ property1: z.string() })` that has been compiled to cjs and esm
// using tsup. This build occurs in the CodeSandbox.
import { baseSchema as baseSchemaCjs } from "../external_package_mock_dist/index.cjs";
import { baseSchema as baseSchemaEsm } from "../external_package_mock_dist/index.js";
import { z } from "zod";

it("imported cjs zod schema parse - no extra properties (PASS)", () => {
  expect(() =>
    baseSchemaCjs.parse({
      property1: "1",
    }),
  ).not.toThrow();
});

it("imported cjs zod schema parse - extra properties (PASS)", () => {
  expect(() =>
    baseSchemaCjs.parse({
      property1: "1",
      extraProperty: "str",
    }),
  ).not.toThrow();
});

it("imported cjs zod schema merged parse - extra properties (FAIL)", () => {
  const schema = baseSchemaCjs.merge(
    z.object({
      property2: z.string(),
    }),
  );

  expect(() =>
    schema.parse({
      property1: "str",
      property2: "str",
      extraProperty: "str",
    }),
  ).not.toThrow();
});

it("imported esm zod schema merged parse - extra properties (PASS)", () => {
  const schema = baseSchemaEsm.merge(
    z.object({
      property2: z.string(),
    }),
  );

  expect(() =>
    schema.parse({
      property1: "str",
      property2: "str",
      extraProperty: "str",
    }),
  ).not.toThrow();
});

it("locally created zod schema merged parse - extra properties (PASS)", () => {
  const baseSchemaLocal = z.object({
    property1: z.string(),
  });
  const schema = baseSchemaLocal.merge(
    z.object({
      property2: z.string(),
    }),
  );

  expect(() =>
    schema.parse({
      property1: "str",
      property2: "str",
      extraProperty: "str",
    }),
  ).not.toThrow();
});

Expected result

According to the docs, By default Zod object schema strip unknown keys from the output.

bvandercar-vt avatar Dec 07 '23 19:12 bvandercar-vt

Note: adding this to the Vite config fixes the issue:

optimizeDeps: {
      exclude: ['zod'],
    },
test: {
      alias: {
        zod: path.resolve('./node_modules/zod/lib/index.js'),
      },
}

But hoping the issue can be solved at its core.

UPDATE: No longer think that this issue is only in Vite. Updated the Codesandbox-- am seeing the issue even when not using Vitest.

bvandercar-vt avatar Dec 07 '23 19:12 bvandercar-vt

Possibly related: https://github.com/colinhacks/zod/issues/222

bvandercar-vt avatar Dec 07 '23 19:12 bvandercar-vt

I am experiencing the same issue in a react cra app:

// Myform.rules.ts
import { mySchema } from "../../mySchema"
import { zodResolver } from "@mantine/form";
import { z } from "zod";

const myFormSchema = mySchema.merge(
  z.object({
    check: z.boolean().optional(),
  })
);

export type MyValues = z.infer<typeof myFormSchema>;

export const rules = zodResolver(myFormSchema);



// MyForm.tsx

...
 const form = useForm<MyValues>({
    initialValues: myItem
      ? {...myItem}
      : undefined,
    validate: rules,
  });

This results in form not being valid: id: 'Expected never, received string' Code with same structure but no merge works fine

~! The issue does not appear when using extend instead of merge

fjtheknight avatar Jan 21 '24 11:01 fjtheknight

+1 same here. Receiving expected "never", received "object" at runtime when using a bundled version of my app built with esbuild and running on Node 20 in a Lambda environment. Before bundling node_modules in my app it worked well

Kayyow avatar Mar 11 '24 17:03 Kayyow

I’m also having this issue with vitest for my tests. No problem using tsx for starting the app. Only the tests are failing.

qraynaud avatar Apr 05 '24 12:04 qraynaud