CarterGrimmeisen / zod-prisma

A custom prisma generator that creates Zod schemas from your Prisma model.
MIT License
826 stars 86 forks source link

Schema type inference & Prisma types differer when using optional fields #90

Open franky47 opened 2 years ago

franky47 commented 2 years ago

The 0.5.x releases and the change from .nullable() to .nullish() for optional fields have revealed a potential flaw in my application logic. Allow me to elaborate (this is not an issue per-se, but it can help others in a similar situation).

Optional fields in Prisma behave differently when reading and writing.

Let's use the following example model:

model User {
  id   String  @id @default(uuid())
  name String?
}

When writing data to the database (using create/update/upsert model methods), the following input types are accepted:

Value type on create on update/upsert
string insert value replace value
null keep field empty make field empty
undefined keep field empty no-op

This is because the UserCreateInput type in Prisma looks like this:

export type UserCreateInput = {
  id?: string 
  name?: string | null
}

On the other hand, when querying data, there are only two data types returned:

Note that there is no undefined on queried data.

This is because the User type in Prisma looks like this:

export type User = {
  id: string
  name: string | null
}

I agree that the generated schemas should represent what Prisma will accept as input, in order to do pre-write validation.

However, I use z.TypeOf<typeof aSubsetOfASchema> to generate partial types from the schemas, and have this single source of truth be used for:

Example:

// On the server:

export const userNameReply = userSchema.pick({
  name: true
})

export type UserNameReply = z.TypeOf<typeof userNameReply>

fastify.get<{ Reply: UserNameReply }>(
  '/user/name', 
  {
    schema: {
      reply: {
        200: zodToJsonSchema(userNameReply)
      }
    }
  },
  async (req, res) => {
    const user = await req.server.prisma.user.findUnique({ where: {...} })
    // The JSON schema on the reply guarantees only the `name` property is returned
    return user
  }
)

// In the client:

const res = await axios.get('/user/name')
const userName = userNameReply.parse(res.data)

// or

const res = await axios.get<UserNameReply>('/user/name')
const userName = res.data

Because the userNameReply schema has .nullish() optional fields, it adds incorrect undefined constraints to the response data type.

As I said, this is not really an issue with zod-prisma and more with the way I use it, but I thought it could open a discussion on this duality of input/output types in Prisma queries.

Note: I know of the z.input and z.output type inferring helpers, but those probably won't help here as they are internal to the schema structure.

zomars commented 2 years ago

Maybe this could be solved in zod by adding a way to remove null or undefined values from a schema.

CarterGrimmeisen commented 2 years ago

I agree that this is potentially something to address and that the nullish change made things a little bit unclear.

For me, the use case was always as a form of pre-validation when creating new records. For updating you can always just take the schema and run '.partial()' on it to selectively make fields optional.

One idea I had is to make different schemas for the different Prisma operations. (i.e. one for create, update, and one for querying). I don't think this is all that difficult to do given the information we get from the Prisma DMMF but it would certainly require some work.

Let me know what you think or if you have any additional ideas.