zod icon indicating copy to clipboard operation
zod copied to clipboard

Derived arguments (computed props) within a schema

Open jonlambert opened this issue 3 years ago • 3 comments

Hey everyone!

Firstly – Zod is awesome. Picked it up recently and I've been having a blast working with it. In recent weeks I've encountered a situation which I thought might benefit from a discussion here.

We're currently in the process of refactoring an old codebase that used mobx-state-tree to manage it's state. MST has a really similar approach to Zod's runtime type system, with a nice API for adding computed properties to a schema:

const UserStore = types
  .model({
    users: types.array(User)
  })
  .views(self => ({
    get numberOfAdults() {
      return self.users.filter(user => user.age > 18).length
    }
  }))

Now, I came across a good suggestion to wrap Zod's schema.parse({}) call in a factory function and add the computed functions in before returning:

const UserSchema = z.object({ name: z.string(), age: z.number() })

const UserCollectionSchema = z.object({
  users: z.array(UserSchema),
})

const createUserModel = (input: Partial<z.infer<typeof UserCollectionSchema>>) => {
  const model = UserCollectionSchema.parse(input)
  return {
    ...model,
    get numberOfAdults() {
      return model.users.filter((user) => user.age > 18).length
    },
  }
}

createUserModel({ users: [{ name: 'Jon', age: 29 }] }).numberOfAdults // => 1

This is a really clean suggestion, and would definitely be the route I'd look to take. However... consider the following change to the user schema:

const UserSchema = z.object({ name: z.string(), dateOfBirth: z.date() })

Ideally I'd be able to expose age as a computed property, deriving its value from the date of birth field. This is difficult to achieve without nesting factories here - the consumer of UserCollectionSchema shouldn't care whether age is a derived property or not. The power here is in nested object schema's each adding their own computed properties that can be carried around alongside the validation logic.

I'm pretty new to using Zod - this might be entirely outside the scope of the project, or something Zod supports now/in the future. There seems to be a few outstanding discussions that relate to this subject but all seem either incomplete or stale, so I thought it was worth asking!

(Possibly related: https://github.com/colinhacks/zod/issues/38)

jonlambert avatar Oct 03 '22 11:10 jonlambert

Actually, seems this is pretty simple to do with transform:

const UserSchema = z
  .object({ name: z.string(), dateOfBirth: z.number() })
  .transform(values => ({ 
    ...values,
    get age() {
      dayjs(values.dateOfBirth).diff(dayjs(), 'year')
    }
  })

jonlambert avatar Oct 08 '22 08:10 jonlambert

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] avatar Dec 07 '22 12:12 stale[bot]

@johlampert as an MST fanboy I am wondering how you deal with the other things MST offers, are you adding any reactivity to your zod objects? Plain mobx? How are you making sure the "views" are reactive?

jrmyio avatar Dec 07 '22 20:12 jrmyio

Has this issue been resolved?

JacobWeisenburger avatar Jan 04 '23 03:01 JacobWeisenburger