colinhacks / zod

TypeScript-first schema validation with static type inference
https://zod.dev
MIT License
33.09k stars 1.15k forks source link

Recursive types with an intermediate object #3672

Open coreyjv opened 1 month ago

coreyjv commented 1 month ago

Suppose I have a structure like the following:

const sample = {
  type: 'bulletList',
  content: [
    {
      type: 'listItem',
      content: [{ type: 'bulletList', content: [{ type: 'listItem', content: [{ type: 'paragraph' }] }] }]
    },
    {
      type: 'listItem',
      content: [{ type: 'paragraph' }]
    }
  ]
};

The general constraints are as follows:

I've tried to solve this using the recursive types documentation but I can't figure out how to define the types and schemas with this listItem being an "intermediate" type.

Is it possible to do this with Zod? If so, how?

AdventureBeard commented 1 month ago

Parsing a prosemirror document?

I'm having similar issues today. Trying to build a portable validator for my complex prosemirror documents. Made a zod schema that encapsulates everything, but ran into issues with recursion and discriminated unions. Tried using the solutions recommended by others by using the 'satisfies' keyword, but didn't work for me.

I really just need the validation, rather than any of the typechecking, so I bailed out of the type issues by casting the problematic types to z.AnyZodObject. Throws away 75% of what's useful about zod, but the validation is still helpful (and necessary, in my case.)

coreyjv commented 1 month ago

I am not parsing a prosemirror document but it is another document format. After more fiddling I think I got something that may work so I figured I'd share it:

import { z } from 'zod'

const BaseListItem = z.object({ type: z.literal('listItem') })

const Paragraph = z.object({ type: z.literal('paragraph') })

const BaseBulletList = z.object({ type: z.literal('bulletList') })

type BulletList = z.infer<typeof BaseBulletList> & {
  content: ListItem[]
}

type ListItem = z.infer<typeof BaseListItem> & {
  content: (z.infer<typeof Paragraph> | BulletList)[]
}

const ListItem: z.ZodType<ListItem> = BaseListItem.extend({
  content: z.lazy(() => z.array(z.union([BulletList, Paragraph])))
})

const BulletList: z.ZodType<BulletList> = BaseBulletList.extend({
  content: z.lazy(() => ListItem.array())
})

const sample = {
  type: 'bulletList',
  content: [
    {
      type: 'listItem',
      content: [{ type: 'bulletList', content: [{ type: 'listItem', content: [{ type: 'paragraph' }] }] }]
    },
    {
      type: 'listItem',
      content: [{ type: 'paragraph' }]
    }
  ]
}

console.log(JSON.stringify(BulletList.parse(sample), null, 2))

The only issue I've discovered with this is that for other places in my schema definitions that reference these types I'm not longer able to use discriminatedUnions