Repository navigation
Replies: 5 comments 1 reply
|
The const UnknownShape = z.object({
kind: z.literal('unknown'),
});
const Shape = z.discriminatedUnion("kind", [Circle, Rectangle, UnknownShape]);
const PreprocessedShape = z.preprocess(v => {
switch (v.kind) {
case 'circle':
case 'rectangle':
return v;
default:
return {kind: 'unknown'};
}
}, Shape);
const Shapes = z.array(PreprocessedShape);I still need to properly type |
|
Here's a fully typed const PreprocessedShape = z.preprocess(v => {
function hasKind(arg: any): arg is {kind: string} {
if (typeof arg !== 'object') {
throw {
code: 'invalid_type',
expected: 'object',
received: String(arg),
path: [],
message: 'Required',
};
}
if (arg === null) {
throw {
code: 'invalid_type',
expected: 'object',
received: String(arg),
path: [],
message: 'Expected object, received null',
};
}
if (!('kind' in arg)) {
throw {
code: 'invalid_type',
expected: 'string',
received: 'undefined',
path: ['kind'],
message: 'Required',
};
}
if (typeof arg.kind !== 'string') {
throw {
code: 'invalid_type',
expected: 'string',
received: String(arg.kind),
path: ['kind'],
message: `Expected string, received ${String(arg.kind)}`,
};
}
return true;
}
if (!hasKind(v)) {
throw Error('Invalid shape');
}
switch (v.kind) {
case 'circle':
case 'rectangle':
return v;
default:
return {kind: 'unknown'};
}
}, Shape); |
|
An alternative is to piggy-back on a missing const PreprocessedShape = z.preprocess(v => {
function hasKind(arg: any): arg is {kind: string} {
return (
typeof arg === 'object' &&
arg !== null &&
'kind' in arg &&
typeof arg.kind === 'string'
);
}
if (!hasKind(v)) {
return v;
}
switch (v.kind) {
case 'circle':
case 'rectangle':
return v;
default:
return {kind: 'unknown'};
}
}, Shape); |
|
Even simpler, just try to parse the discriminator as const PreprocessedShape = z.preprocess(v => {
const parsed = z.object({kind: z.string()}).parse(v);
switch (parsed.kind) {
case 'circle':
case 'rectangle':
return v;
default:
return {kind: 'unknown'};
}
}, Shape); |
|
I'd lean toward adding a fallback/'unknown' variant to the discriminated union rather than trying to make it truly open - that way a payload with a discriminator value your current schema doesn't recognize yet lands in the unknown branch and gets logged, instead of hard-crashing. More boilerplate up front but it buys real forward compatibility across deploys instead of needing frontend and backend to ship in lockstep. |

Uh oh!
There was an error while loading. Please reload this page.
A common problem with schemas is that the frontend and backend might not be using the same version. For example, we might update the backend to add a new member to a discriminated union but the frontend won't know about it until updated. Updating them in unison isn't possible.
Catching the parsing error isn't workable. The discriminated union might be part of a large structure that would otherwise parse fine. Think of an input that consists of an array of discriminated union values. Most of the values could be of a type the frontend knows about and we want to parse those (and e.g. ignore the rest).
Other parsing systems, like Google's protocol buffers, have solutions for cases like this.
In TypeScript/zod we need the discriminated union to be defined in such a way that parsing will succeed even for unknown variants of the discriminator. At the same time we want to discriminate the type for know values of the discriminator (i.e. we want to be able to use the discriminator in a
switchorifstatement to infer which member of the discriminated union we have.)Here's an example discriminated union:
We have two constraints:
Shapeson an unknown input e.g.{kind: 'square', side: 1.0}.switchstatement onkind.The latter would look something like this:
The above
Shapes.parsedoesn't work due to the unknown discriminator value.I think I want something like
(
passthrough_unknown_discriminantwould be a new method on discriminated unions.)The type would be unchanged but the runtime behavior would be to just not fail parsing on an unknown discriminated and e.g. passthrough its values. Known discriminates would still parse against their schema.
An alternative that I haven't explored would be to run a preprocess step where every unknown discriminant would be mapped to a known discriminant (e.g.
"unknown") that then would be part of the discriminated union. Something like this:Thoughts on a solution?
All reactions