Clean definition of models using Zod schemas

My Current Situation My Zod schemas are my single source of truth regarding the structure of models used in my app because they are definable in a very detailed way and can even be used for runtime validation. Since there are usually multiple versions of each model (with id, when I fetch entities from my api; without id, when i create a new one; ...) I came up with a structure like this:
// base schema having the full scope of props (= result of GET requests)
const ModelSchema = z.object({
id: z.string().uuid(),
name: z.string(),
amount: z.number(),
active: z.boolean(),
});

// everything needed for creating a new entity of the model
const ModelCreateSchema = ModelSchema.omit({ id: true });

// id + some props for updating an existing entity "the PATCH way"
const ModelUpdateSchema = ModelSchema.pick({id: true}).merge(ModelCreateSchema.partial());
// base schema having the full scope of props (= result of GET requests)
const ModelSchema = z.object({
id: z.string().uuid(),
name: z.string(),
amount: z.number(),
active: z.boolean(),
});

// everything needed for creating a new entity of the model
const ModelCreateSchema = ModelSchema.omit({ id: true });

// id + some props for updating an existing entity "the PATCH way"
const ModelUpdateSchema = ModelSchema.pick({id: true}).merge(ModelCreateSchema.partial());
Although this already seemed a bit more complicated than I think it might be possible, it worked for the time being. Since my models are of course a bit more complicated than this example, I want to add refines to them. But when adding one to my base schema, I can't omit or pick them anymore since the whole schema is no longer a ZodObject but a ZodEffect which doesn't support those methods. I mean I could keep my old, unrefined version of that model as the base, and add refines later on on top of each single version of my model (create, update, ...), but that seems way too complicated and brings a bad DX since I should only need to define those things once. The Question Now I want to know how do you guys define your models? Do you also use Zod schemas, and if so, how do you structure them to solve issues like mine? Or do you have a completely different approach?
3 Replies
naz6352
naz635217mo ago
The way that I handled ids was to make this:
const withId = z.object({ id: z.string().cuid() });
const withId = z.object({ id: z.string().cuid() });
And then attach it to anything that needs an id
const BaseThing = z.object({
property: z.string(),
});

const GetThing = BaseThing.merge(withId);
const BaseThing = z.object({
property: z.string(),
});

const GetThing = BaseThing.merge(withId);
I did something similar with updatedAt / createdAt timestamps, which ended up being very helpful when I changed them from z.date() to z.coerce.date() once that feature came out, didn't have to change it in a dozen places. As for your problem with refines, it is possible to add a ZodEffect as a property in a ZodObject, if that helps you solve some of the issue.
const RefinedThing = BaseThing.refine(...);
const OtherThing = z.object({
property: RefinedThing
});
const RefinedThing = BaseThing.refine(...);
const OtherThing = z.object({
property: RefinedThing
});
froxx
froxx17mo ago
Regarding the refinement: I need a refinement on the whole object since I want to add a validation regarding multiple (nested) props My case is something like this
const Schema = z.object({
amount: z.number(),
children: z.array(Schema),
const Schema = z.object({
amount: z.number(),
children: z.array(Schema),
And I want to add a refinement that checks that amount is >= the sum all of childrens' amounts In that case I have to refine my base schema, but can't extend / omit / pick on it later on since refine changes its type to ZodEffect
froxx
froxx17mo ago
Alright, I searched a bit in the Zod discussions on GH and found this thread which 100% nailed my problem with refined schemas: https://github.com/colinhacks/zod/discussions/694 The contributors know it's now optimal, but explained very well there why they chose the way they did, so I actually go this way now: 1. Define base schema 2. Extend / omit the base schema wherever I need it 3. Create a central function to add my .refine to a minimally abstracted version of the schema I need for it, and add it on top of the transformed base schema It's not perfect in regards of DX, but it does what it's supposed to do. Hopefully my comment will gather some feedback though and maybe trigger an update in some future: https://github.com/colinhacks/zod/discussions/694#discussioncomment-4926037
GitHub
Using .extend after doing .superRefine. · Discussion #694 · col...
You can .extend() from a z.object(...), but not if you've added a refinement. Example (playground link): import * as z from 'zod'; const userSchema = z.object({ name: z.stri...