zod icon indicating copy to clipboard operation
zod copied to clipboard

3.20 introduced breaking change for `ZodDiscriminatedUnionDef`

Open freben opened this issue 2 years ago • 5 comments

I just wanted to note that 3.20, while claiming to have no breaking changes, did change the number of type arguments for ZodDiscriminatedUnionDef from 3 to 2. This led to breakages such as https://github.com/StefanTerdell/zod-to-json-schema/issues/31, which may be tricky to navigate in a cross-version-compatible manner.

freben avatar Dec 12 '22 12:12 freben

I export literally everything from types.ts so people have an easier time building things on top of Zod. The downside is that there is virtually no change I can make that won't be "breaking" if someone relies on a particular type alias. For that reason, I consider the public API to be the z., the method signatures, and the internal structure of ZodError - the things that a typical user will interact with.

But historically I haven't considered the structure of Defs or the generic type signatures of each subclass part of the public API, hence "no breaking changes". This definitely makes it tricky to I'll take more care to minimize changes to the internals to account for the growing ecosystem of tools that rely on Zod.

That said, 3.20 is a very major release for Zod. I think it's likely that Zod will never move past v3, since it's been a year and a half, and I haven't encountered any reason to do a big rewrite or (public) API breakage. Despite that fact that its "minor", 3.20 is a really big release, I think the best move for ecosystem authors is to treat this as a major release, update to Zod 3.20, and specify zod@^3.20.0 in their peerDeps.

colinhacks avatar Dec 12 '22 19:12 colinhacks

@colinhacks Why not release it as a 4, if you yourself consider it a big release? Not like you are running out of numbers any time soon.

kibertoad avatar Dec 12 '22 19:12 kibertoad

It's not worth the disruption to users in my opinion. 98% of users don't know enough about Zod internals to understand the "breakages" even if I explained them, even with a big release like this one. Upgrading to a new major is stressful.

colinhacks avatar Dec 12 '22 20:12 colinhacks

I still count public types as part of the contract and standard semver expectations were arguably broken here. But for us, it's mainly that one library that bit us and it seems to be maintained, so I'm sure we'll be in the clear soon when they release a new version. Not a big deal. Just know that this approach implies a form of pain and risk as well. Both in terms of us getting compilation errors just as an effect of a renovate-bot minor bump, and in terms of that utility library likely going to have a hard time supporting both pre-3.20 and post-3.20 at the same time.

freben avatar Dec 12 '22 22:12 freben

In case anyone is wondering how to support < 3.20 and >= 3.20 with no type errors (IE if your library parses discriminated union defs using zod as a peer dependency), you can check if the def has optionsMap as a property.

If it does, it's 3.20 use that as the options map, if it doesn't options is the options map and you should use that. Here's a way to do it:

type OptionsMap = Map<Primitive, AnyZodObject>;

type ZodDiscriminatedUnionThreePointTwenty = {
  optionsMap: OptionsMap;
  discriminator: string;
};

type ZodDiscriminatedUnionPreThreePointTwenty = {
  options: OptionsMap;
  discriminator: string;
};

type ZodDiscriminatedUnionDefUnversioned =
  | ZodDiscriminatedUnionPreThreePointTwenty
  | ZodDiscriminatedUnionThreePointTwenty;

function isZodThreePointTwenty(
  def: ZodDiscriminatedUnionDefUnversioned
): def is ZodDiscriminatedUnionThreePointTwenty {
  return "optionsMap" in def;
}

function makeDefConsistent(
  def: ZodDiscriminatedUnionDefUnversioned
): {
  typeName: ZodFirstPartyTypeKind.ZodDiscriminatedUnion;
  discriminator: string;
  options: Map<Primitive, AnyZodObject>;
} {
  const optionsMap = isZodThreePointTwenty(def) ? def.optionsMap : def.options;
  return {
    typeName: ZodFirstPartyTypeKind.ZodDiscriminatedUnion,
    discriminator: def.discriminator,
    options: optionsMap
  };
}

console.log("def:");
console.log(makeDefConsistent(discUnion._def));

Should work, typescript says it works ¯_(ツ)_/¯. I still had to use as any to pass in the function parameter elsewhere in my code though just a headsup. ALSO idk if this explodes in some situations or not, just a starting point

iway1 avatar Dec 13 '22 23:12 iway1

Is this conversation still ongoing? If not, I'd like to close this issue.

JacobWeisenburger avatar Jan 01 '23 12:01 JacobWeisenburger