When working with legacy data, flexible APIs, or lenient input formats (e.g., accepting both "123" and 123, or loose string dates alongside ISO strings), we often need schemas that accept multiple "permissive" types on input, but strictly normalize and output a single "canonical" type.
Currently, standard Codec require strict bi-directional mapping between an "in" schema and an "out" schema. Handling permissive variants currently requires manual handling of the codec, which breaks encapsulation and loses the benefits of the Codec abstraction when passing it across boundaries.
I would like to propose a concept for "Permissive Codecs".
A Permissive Codec allows defining additional "permissive" input and output schemas alongside the primary "canonical" "in" and "out" schemas.
Currently a codec produces the following types:
const dateCodec = z.codec(z.union([z.date(), z.instanceof(Temporal.Instant)]), z.string(), {
// dateOrInstant: `Date | Temporal.Instant`, return `string`
decode: (dateOrInstant) => dateOrInstant instanceof Date ? dateOrInstant.toISOString() : dateOrInstant.toString(),
// string: `string`, return `Date | Temporal.Instant`
encode: (string) => Temporal.Instant.from(string),
});
// decoded: `string`, decodeInput: `Date | Temporal.Instant`
const decoded = dateCodec.decode(decodeInput);
// encoded: `Date | Temporal.Instant`, encodeInput: `string`
const encoded = dateCodec.encode(encodeInput);
Notice how the encoded type is Date | Temporal.Instant even though it logically can only be Temporal.Instant, because the encode function only produces that output. So in this example I want to be able to put Date | Temporal.Instant into and codec to produce a string, but when putting in a string I want to get just the Temporal.Instant.
In other words the "canonical" "in" schema is z.instanceof(Temporal.Instant) and the "permissive" "in" schema is z.date(). In this example the "out"schema z.string() is "canonical" while there is no "permissive" "out" schema. Which means the current standard codec is a permissive codec with just "canonical" schemas and without any permissive schemas.
Conceptual API Example:
const dateCodec = z.permissive({
in: z.instanceof(Temporal.Instant),
inPermissive: z.date(),
out: z.string(),
}, {
// dateOrInstant: `Date | Temporal.Instant`, return `string`
decode: (dateOrInstant) => dateOrInstant instanceof Date ? dateOrInstant.toISOString() : dateOrInstant.toString(),
// string: `string`, return `Temporal.Instant`
encode: (string) => Temporal.Instant.from(string),
});
// decoded: `string`, decodeInput: `Date | Temporal.Instant`
const decoded = dateCodec.decode(decodeInput);
// encoded: `Temporal.Instant`, encodeInput: `string`
const encoded = dateCodec.encode(encodeInput);
Notice how the decode function in the z.permissive function takes a union of the in and inPermissive type and returns the out type, while the encode function takes the out and the here absent outPermissive type and returns the in type.
One type which would immediately benefit from this is the z.stringbool() codec, because the behavior could be described without ambiguity. The docs give the example:
const stringbool = z.stringbool({ truthy: ["yes", "y"], falsy: ["no", "n"] });
stringbool.encode(true); // => "yes"
stringbool.encode(false); // => "no"
In this case a permissive codec could define z.literal("yes").or(z.literal("no") as the "canonical" type and all other values as the "permissive" type for a less ambiguous type experience with codecs.
When working with legacy data, flexible APIs, or lenient input formats (e.g., accepting both
"123"and123, or loose string dates alongside ISO strings), we often need schemas that accept multiple "permissive" types on input, but strictly normalize and output a single "canonical" type.Currently, standard
Codecrequire strict bi-directional mapping between an "in" schema and an "out" schema. Handling permissive variants currently requires manual handling of the codec, which breaks encapsulation and loses the benefits of the Codec abstraction when passing it across boundaries.I would like to propose a concept for "Permissive Codecs".
A Permissive Codec allows defining additional "permissive" input and output schemas alongside the primary "canonical" "in" and "out" schemas.
Currently a codec produces the following types:
Notice how the
encodedtype isDate | Temporal.Instanteven though it logically can only beTemporal.Instant, because theencodefunction only produces that output. So in this example I want to be able to putDate | Temporal.Instantinto and codec to produce astring, but when putting in astringI want to get just theTemporal.Instant.In other words the "canonical" "in" schema is
z.instanceof(Temporal.Instant)and the "permissive" "in" schema isz.date(). In this example the "out"schemaz.string()is "canonical" while there is no "permissive" "out" schema. Which means the current standard codec is a permissive codec with just "canonical" schemas and without any permissive schemas.Conceptual API Example:
Notice how the
decodefunction in thez.permissivefunction takes a union of theinandinPermissivetype and returns theouttype, while theencodefunction takes theoutand the here absentoutPermissivetype and returns theintype.One type which would immediately benefit from this is the
z.stringbool()codec, because the behavior could be described without ambiguity. The docs give the example:In this case a permissive codec could define
z.literal("yes").or(z.literal("no")as the "canonical" type and all other values as the "permissive" type for a less ambiguous type experience with codecs.