zod icon indicating copy to clipboard operation
zod copied to clipboard

keep undefined values in .record()

Open JacobWeisenburger opened this issue 2 years ago • 2 comments

Discussed in https://github.com/colinhacks/zod/discussions/2760

Originally posted by DanielHoffmann September 22, 2023 is there a way where I can type a record while keeping the keys that have undefined as a value?

For example, how could I set up this schema so this test passes?

  import { z } from 'zod'
  test('allow undefined values', () => {
    const keySchema = z.string().startsWith('_')
    const valueSchema = z.union([z.number(), z.undefined()])
    const schema = z.record(keySchema, valueSchema)
    expect(
      Object.keys(
        schema.parse({
          _test: undefined,
        }),
      ),
    ).toEqual(['_test'])
  })

in this case the Object.keys(schema.parse(...)) is returning empty array

Could this be a bug? For example, this tests passes:

import { z } from 'zod'
test('allow undefined values', () => {
    const schema = z.object({
      a: z.union([z.number(), z.undefined()]),
    })
    expect(
      Object.keys(
        schema.parse({
          a: undefined,
        }),
      ),
    ).toEqual(['a'])
})

I would kind of expect .record() and .object() to behave the same in regards to the values

JacobWeisenburger avatar Sep 22 '23 13:09 JacobWeisenburger

@DanielHoffmann, I have confirmed this code works the way you said and it does appear to be a bug. Thanks for reporting it.

JacobWeisenburger avatar Sep 22 '23 14:09 JacobWeisenburger

Just going to throw a walk-around here for anyone else stumbles on this.

const undef = Symbol('undef');
const record = (val: z.ZodType) =>
  z
    .record(
      z.string(),
      z.preprocess((v) => (v == undefined ? undef : v), z.union([val, z.symbol(undef), z.undefined()])),
    )
    .transform((v) => {
      for (const key in v) {
        if (v[key] === undef) v[key] = undefined;
      }
      return v;
    });

const x = record(z.number()).parse({ a: undefined });

Louis-Tian avatar Sep 12 '24 06:09 Louis-Tian

Hi, @JacobWeisenburger. I'm Dosu, and I'm helping the Zod team manage their backlog. I'm marking this issue as stale.

Issue Summary:

  • You reported a discrepancy in Zod's .record() method, which removes keys with undefined values, unlike .object().
  • This behavior was confirmed as a bug after a report by DanielHoffmann.
  • Louis-Tian provided a workaround using a custom symbol to preserve undefined values in .record(), which was well-received by the community.

Next Steps:

  • Please let me know if this issue is still relevant to the latest version of Zod. If so, you can keep the discussion open by commenting on the issue.
  • Otherwise, the issue will be automatically closed in 14 days.

Thank you for your understanding and contribution!

dosubot[bot] avatar Jul 24 '25 16:07 dosubot[bot]

The issue is still relevant and moreover, it doesn't work for z.object as well:

z.object({ a: z.number().or(z.undefined()) }).parse({ a: undefined }); // expected: { a: undefined }, actual: {}

mifopen avatar Jul 29 '25 07:07 mifopen

Hi, @JacobWeisenburger. I'm Dosu, and I'm helping the Zod team manage their backlog and am marking this issue as stale.

Issue Summary:

  • You reported that .record() removes keys with undefined values during parsing, unlike .object() which keeps them.
  • This behavior was confirmed as a bug by DanielHoffmann.
  • A workaround using a custom symbol to preserve undefined values in .record() was shared by Louis-Tian.
  • The issue remains unresolved and relevant, with similar behavior noted in .object() by another user.
  • I have marked the issue as stale pending further input.

Next Steps:

  • Please let me know if this issue is still relevant with the latest version of Zod by commenting here to keep the discussion open.
  • Otherwise, this issue will be automatically closed in 7 days.

Thanks for your understanding and contribution!

dosubot[bot] avatar Nov 04 '25 16:11 dosubot[bot]

Please reopen. The issue is still relevant.

mifopen avatar Nov 12 '25 11:11 mifopen