Zod: How to dynamically generate a schema?
Asked Answered
U

2

7

I desperately tried to generate a zod schema dynamically without success.

I have coded a kind of reusable form with react-hook-form, allowing me to easily create forms with various number of inputs. This component needs an array of fields, containing all the needed information to create them (type, label, classname, etc...).

In order to provide my form the right types I need to provide it my fields type to theuseForm<FieldValues>() method. That's why I need to create a schema dynamically.

// Schema manually coded
const schema = z.object({
    foo: z.string(),
    bar: z.number()
})

type Fields = z.infer<typeof schema>
// { foo: string; bar: number; }
// Schema dynamically generated
const fields = [
    {
        name: "foo",
        fieldType: z.string(),
        //...
    },
    {
        name: "bar",
        fieldType: z.number(),
        //...
    }
]

const generateSchemaFromFields = (fields) => {
    // ?
}

const generatedSchema = generateSchemaFromFields(fields)

type GeneratedFields = z.infer<typeof generatedSchema>
// { foo: string; bar: number; }

Any idea? Thanks in advance!

edit: I rewrote the question for clarity

Underpass answered 11/4, 2023 at 9:9 Comment(2)
z.object is called with Record<string, ZodTypeAny>, and that means all schemas are consider as ZodTypeAny no matter what it was, to solve this will use lots of generic to keep the type information. And my personal suggestion is use object instead of array to store the schema in this case, it will be easier to inference the typeCrat
Any luck on this? I'm currently facing a similar issue.Willywillynilly
S
7

I am not sure I understand what you want from this:

  1. A parser that makes sure the values are correct according to the form's specification.
  2. Helpful type inference from the schema for development.

The first should be achievable. I'll show an example below. The second, however, might be impossible.

You wrote that the form is defined dynamically. I understand this so that the information is available at runtime. That means that type GeneratedFields = z.infer<typeof generatedSchema> won't help you much during development, because TypeScript doesn't know the actual definition but only all possible definitions. As Jerryh001 mentioned, the type will be rather generic like Record<string, any>.

In case you just need a parser that checks the types of the form field data at runtime:

const fields = [
  {
    name: 'foo',
    fieldType: z.string(),
  },
  {
    name: 'bar',
    fieldType: z.number(),
  },
] as { name: string; fieldType: ZodSchema }[];

// Convert the fields definition into an object ({ [name]: fieldType })
// and turn that into a schema. Keep in mind that `name` should be unique.
const schema = z.object(
  Object.fromEntries(
    fields.map((field) => [field.name, field.fieldType])
  )
)

const validInput = { foo: 'bar', bar: 1 }
expect(schema.safeParse(validInput).success).toBe(true)

const invalidInput = { foo: 1, bar: 'err' }
expect(schema.safeParse(invalidInput).success).toBe(false)

The resulting schema will have a rather useless type:

z.ZodObject<{
    [k: string]: ZodSchema<any, z.ZodTypeDef, any>;
}, "strip", z.ZodTypeAny, {
    [x: string]: any;
}, {
    [x: string]: any;
}>

which is basically Record<string, any> but .safeParse() will deliver useful error messages, if anything goes wrong and you want to communicate this to the user.

Smithery answered 14/9, 2023 at 15:3 Comment(3)
What makes you say that the second option might be impossible?Delenadeleon
I stated that achieving a helpful type inference for the development is potentially impossible, because I consider a type of Record<string, any> as not helpful. The type in the end depends on the constraints that are applied to the dynamically generated schema. If you allow only a handful of field types , then it can be helpful. If you allow any type, then I consider it not useful. That is a personal opinion.Smithery
After a lot of attempts at making highly generic and composable forms, I agree. You can use a dynamic combination of zod schemas to generate your types, but it's hard if you don't have any hardcoded zod schemas.Delenadeleon
J
1

For anyone coming back to this, I figured out a solution for dynamically generating zod schemas:

My object array to generate a schema from:

const formItems = [{
    name: "whatIsYourName",
    label: "What is your name?",
    description: "This is your name, you should know it.",
    type: "text",
    validation: [
        {
            type: "min",
            value: 2,
            message: "Must be at least 2 chars"
        }
    ]
}]

I'm using react-hook-form which op seems to be using too based on the question so I initialize my form with:

const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(generateZodSchema(formItems)),
    defaultValues: {
        whatIsYourName: ""
    }
})

My generateZodSchema method:

function generateZodSchema(formItems: object) {
const schema: Record<string, z.ZodType<any>> = {};

formItems.forEach((item) => {
    let fieldSchema = z.string()

    item.validation.forEach((rule) => {
        switch(rule.type) {
            case 'min':
                fieldSchema = fieldSchema.min(rule.value, {message: rule.message})
                break;
            default:
                throw new Error("Unsupported validation type")
        }
    })

    schema[item.name] = fieldSchema;
})

return z.object(schema)

}

This is a basic example with just one validation type, however, you can extend this by creating a validationRule type and a FieldConfig type instead of declaring formItems as an object etc...

You'll also want to add some code to set the correct type e.g. z.string()

Juncaceous answered 31/7, 2024 at 22:57 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.