Skip to content

Instantly share code, notes, and snippets.

@Yotamho
Created November 18, 2025 14:43
Show Gist options
  • Select an option

  • Save Yotamho/9a05f8c41a8959168cc2b274b0585b1c to your computer and use it in GitHub Desktop.

Select an option

Save Yotamho/9a05f8c41a8959168cc2b274b0585b1c to your computer and use it in GitHub Desktop.
A helper that converts a Zod discriminated union into a Payload CMS "blocks" field
import { z, ZodLiteral, type ZodObject } from 'zod/v4'
import type { Field as PayloadField, Block } from 'payload'
type BlocksField = Extract<PayloadField, { type: 'blocks' }>
/**
* Convert a Zod discriminated union into a Payload "blocks" field.
* Assumptions (not validated):
* - Each option is: { [discriminator]: z.literal('<slug>'), body: z.object({ [k]: z.string() }) }
* - "body" keys are non-optional strings only.
* - The discriminator is a non-optional string literal, and its key name is provided.
*/
export function zodDUToBlocksField(
name: string,
discriminator: string,
du: z.ZodDiscriminatedUnion<readonly ZodObject<Record<string, z.ZodType>>[]>,
): BlocksField {
// Zod v3 exposes `options` publicly (array or Map depending on version).
const optionObjs: ZodObject<Record<string, z.ZodType>>[] = Array.isArray(du.options)
? du.options
: [...du.options.values()]
const blocks: Block[] = optionObjs.map((opt) => {
// 1) slug from the discriminator literal
const disc = opt.shape[discriminator] as ZodLiteral<string>
const slug = disc.value
// 2) fields from body object (strings only, per assumption)
const body = opt.shape.body as ZodObject<Record<string, z.ZodString>>
const fields: PayloadField[] = Object.keys(body.shape).map(
(key) =>
({
name: key,
type: 'text',
required: true,
}) as const satisfies PayloadField,
)
return {
slug,
labels: { singular: slug, plural: `${slug}s` },
fields,
}
})
return {
name,
type: 'blocks',
required: true,
minRows: 1,
blocks,
} as const
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment