zod-invertible
Version:
declare zod schemas that can be inverted to format from output to input
178 lines (170 loc) • 5.82 kB
text/typescript
import z from 'zod'
export interface ZodInvertibleDef<
I extends z.ZodTypeAny,
O extends z.ZodTypeAny,
> extends z.ZodPipelineDef<z.ZodEffects<I, z.input<O>>, O> {
inputSchema: I
outputSchema: O
parse: (
value: z.output<I>,
ctx: z.RefinementCtx
) => z.input<O> | Promise<z.input<O>>
format: (
value: z.output<O>,
ctx: z.RefinementCtx
) => z.output<I> | Promise<z.output<I>>
}
export class ZodInvertible<
I extends z.ZodTypeAny,
O extends z.ZodTypeAny,
> extends z.ZodPipeline<z.ZodEffects<I, z.input<O>>, O> {
declare _def: ZodInvertibleDef<I, O>
constructor(
inputSchema: I,
parse: (
out: z.output<I>,
ctx: z.RefinementCtx
) => z.input<O> | Promise<z.input<O>>,
outputSchema: O,
format: (
out: z.output<O>,
ctx: z.RefinementCtx
) => z.output<I> | Promise<z.output<I>>
) {
super({
...inputSchema.transform(parse).pipe(outputSchema)._def,
inputSchema,
outputSchema,
parse,
format,
} as ZodInvertibleDef<I, O>)
}
}
export function invertible<T extends z.ZodTypeAny, U extends z.ZodTypeAny>(
schema: T,
parse: (
value: z.output<T>,
ctx: z.RefinementCtx
) => z.input<U> | Promise<z.input<U>>,
outSchema: U,
format: (
value: z.output<U>,
ctx: z.RefinementCtx
) => z.output<T> | Promise<z.output<T>>
) {
return new ZodInvertible(schema, parse, outSchema, format)
}
export const IgnoreEffect = Symbol('IgnoreEffect')
export function ignoreEffect<T extends z.ZodEffects<any, any, any>>(
schema: T
): T {
;(schema as any)[IgnoreEffect] = true
return schema
}
export function invert<T extends z.ZodTypeAny>(
schema: T
): z.ZodType<z.input<T>, any, z.output<T>> {
switch (schema._def.typeName) {
case z.ZodFirstPartyTypeKind.ZodArray:
return z.array(invert((schema as any as z.ZodArray<any>).element))
case z.ZodFirstPartyTypeKind.ZodObject: {
const { shape, _def } = schema as any as z.AnyZodObject
const invertedShape = Object.fromEntries(
Object.entries(shape).map(([key, value]) => [key, invert(value as any)])
)
return new z.ZodObject({
..._def,
catchall: invert(_def.catchall),
shape: () => invertedShape,
})
}
case z.ZodFirstPartyTypeKind.ZodUnion:
return z.union((schema as any as z.ZodUnion<any>).options.map(invert))
case z.ZodFirstPartyTypeKind.ZodDiscriminatedUnion: {
const { discriminator, options } =
schema as any as z.ZodDiscriminatedUnion<any, any>
return z.discriminatedUnion(discriminator, options.map(invert))
}
case z.ZodFirstPartyTypeKind.ZodIntersection: {
const { left, right } = (schema as any as z.ZodIntersection<any, any>)
._def
return z.intersection(invert(left), invert(right))
}
case z.ZodFirstPartyTypeKind.ZodTuple: {
const {
items,
_def: { rest },
} = schema as any as z.ZodTuple<any, any>
const result = z.tuple(items.map(invert))
return rest ? result.rest(invert(rest)) : result
}
case z.ZodFirstPartyTypeKind.ZodRecord: {
const { keySchema, valueSchema } = schema as any as z.ZodRecord<any, any>
return z.record(invert(keySchema), invert(valueSchema))
}
case z.ZodFirstPartyTypeKind.ZodMap: {
const { keySchema, valueSchema } = schema as any as z.ZodMap<any, any>
return z.map(invert(keySchema), invert(valueSchema))
}
case z.ZodFirstPartyTypeKind.ZodSet:
return z.set(invert((schema as any as z.ZodSet<any>)._def.valueType))
case z.ZodFirstPartyTypeKind.ZodFunction: {
const {
_def: { args, returns },
} = schema as any as z.ZodFunction<any, any>
return z.function(args.map(invert), invert(returns))
}
case z.ZodFirstPartyTypeKind.ZodLazy:
return z.lazy(() => invert((schema as any as z.ZodLazy<any>).schema))
case z.ZodFirstPartyTypeKind.ZodEffects: {
const {
_def: { effect, schema: innerType },
} = schema as any as z.ZodEffects<any>
switch (effect.type) {
case 'refinement': {
const { refinement } = effect
return z.any().superRefine(refinement).pipe(invert(innerType))
}
case 'preprocess':
case 'transform':
if ((schema as any)[IgnoreEffect]) {
return invert(innerType)
}
throw new Error(`effect not supported: ${effect.type}`)
}
break
}
case z.ZodFirstPartyTypeKind.ZodOptional:
return invert((schema as any as z.ZodOptional<any>).unwrap()).optional()
case z.ZodFirstPartyTypeKind.ZodNullable:
return invert((schema as any as z.ZodNullable<any>).unwrap()).nullable()
case z.ZodFirstPartyTypeKind.ZodDefault:
return invert((schema as any as z.ZodDefault<any>).removeDefault())
case z.ZodFirstPartyTypeKind.ZodCatch:
return invert((schema as any as z.ZodCatch<any>).removeCatch())
case z.ZodFirstPartyTypeKind.ZodPromise:
return z.promise(invert((schema as any as z.ZodPromise<any>)._def.type))
case z.ZodFirstPartyTypeKind.ZodBranded:
return invert((schema as any as z.ZodBranded<any, any>)._def.type)
case z.ZodFirstPartyTypeKind.ZodPipeline: {
if (schema instanceof ZodInvertible) {
const {
_def: { inputSchema, outputSchema, parse, format },
} = schema
return invertible(
invert(outputSchema),
format,
invert(inputSchema),
parse
)
}
const { _def } = schema as any as z.ZodPipeline<any, any>
return invert(_def.out).pipe(invert(_def.in))
}
case z.ZodFirstPartyTypeKind.ZodReadonly:
return invert(
(schema as any as z.ZodReadonly<any>)._def.innerType
).readonly()
}
return schema
}