zod icon indicating copy to clipboard operation
zod copied to clipboard

superRefine breaks compatibility with discriminatedUnion

Open cdaringe opened this issue 2 years ago • 9 comments

Problem

Here's an easy demo, using [email protected]

import * as z from "zod";

const schemaA = z.object({
  version: z.literal("a"),
}); // observation, no use of superRefine

z.discriminatedUnion("version", [schemaA]); // works

const schemaB = z
  .object({
    version: z.literal("b"),
  })
  .superRefine(() => {}); // observation, use of superRefine

z.discriminatedUnion("version", [schemaB]); // doesn't work

/*
Type 'ZodEffects<ZodObject<{ version: ZodLiteral<"b">; }, "strip", ZodTypeAny, { version: "b"; }, { version: "b"; }>, { version: "b"; }, { version: "b"; }>' is missing the following properties from type 'ZodObject<{ version: ZodTypeAny; } & ZodRawShape, UnknownKeysParam, ZodTypeAny, { [x: string]: any; version?: any; }, { [x: string]: any; version?: any; }>': _cached, _getCached, shape, strict, and 14 more.ts(2740)
*/

The type-error is correct, and it breaks at runtime as well.

In otherwords, ZodEffect<ZodObject> isn't supported... but it should be! A schema that has assertions on top of it is still a compatible schema.

Expectation

z.discriminatedUnion to still work on .superRefined schemas.

cdaringe avatar May 17 '23 18:05 cdaringe

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] avatar Aug 16 '23 01:08 stale[bot]

ah jamon. have had a patch open for this issue for months

cdaringe avatar Aug 16 '23 02:08 cdaringe

Just hit this bug. Any workaround?

jonavila avatar Sep 22 '23 14:09 jonavila

None. I submitted a patch, but it has idled. I ended up just using .union

On Fri, Sep 22, 2023 at 7:26 AM Jonathan Avila @.***> wrote:

Just hit this bug. Any workaround?

— Reply to this email directly, view it on GitHub https://github.com/colinhacks/zod/issues/2440#issuecomment-1731517695, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAHU57OLSAKLULUDI7VMMMDX3WNZXANCNFSM6AAAAAAYFOZBP4 . You are receiving this because you authored the thread.Message ID: @.***>

cdaringe avatar Sep 22 '23 14:09 cdaringe

I have ran in to this as well. I was about to submit my own issue with this example: https://codesandbox.io/p/sandbox/suspicious-bash-4z8ptf but found this issue instead.

johnslemmer avatar Nov 17 '23 00:11 johnslemmer

This will be fixed in Zod 4 per https://github.com/colinhacks/zod/pull/2441#issuecomment-2044094604

Leaving open until then.

colinhacks avatar Apr 09 '24 03:04 colinhacks

I've ran into the same issue, is there a work around?

const SubPartQuestionZodSchema = QuestionSchema.extend({
	Type: z.literal(QuestionTypes.SUB_PART),
	Questions: z.array(z.lazy(() => QuestionZodSchema)),
});

export const QuestionZodSchema = z.discriminatedUnion("Type", [
	FillInTheBlankQuestionZodSchema,
	EssayQuestionZodSchema,
	SubPartQuestionZodSchema,
]);

wanted to add a superRefine to SubPartQuestionZodSchema

const SubPartQuestionZodSchema = QuestionSchema.extend({
	Type: z.literal(QuestionTypes.SUB_PART),
	Questions: z.array(z.lazy(() => QuestionZodSchema)),
}).superRefine((data, ctx) => {
	if (data.QuestionLevel !== QuestionLevel.SUBJECT) {
		if (!data.Metadata.Subjects || data.Metadata.Subjects.length <= 0) {
			ctx.addIssue({
				code: z.ZodIssueCode.invalid_type,
				expected: "array",
				received: typeof data.Metadata.Subjects,
				message: "Subjects can't be empty",
			});
		}
	}
});

premdasvm avatar Apr 27 '24 22:04 premdasvm

The workaround is to build the ZodDiscriminatedUnion manually :

function zDiscriminatedUnion<T extends readonly [z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]]>(key: string, types: T): z.ZodUnion<T>;
function zDiscriminatedUnion(key: string, types: z.ZodTypeAny[]): any {
  const optionsMap = new Map();
  for(const type of types) {
    const value = (type instanceof z.ZodEffects ? type.sourceType() : type).shape[key];
    if(!(value instanceof z.ZodLiteral) || optionsMap.has(value.value)) {
      throw new Error("cannot contruct discriminated union");
    }
    optionsMap.set(value.value, type);
  }
  return new z.ZodDiscriminatedUnion({typeName: z.ZodFirstPartyTypeKind.ZodDiscriminatedUnion, discriminator: key, options: types as any, optionsMap});
}

sloonz avatar Jun 17 '24 10:06 sloonz