andreww2012 / mongoose-zod

A library which allows to author mongoose ("a MongoDB object modeling tool") schemas using zod ("a TypeScript-first schema declaration and validation library").
MIT License
49 stars 7 forks source link

z.nullable() not respected #3

Closed MarijnMensinga closed 9 months ago

MarijnMensinga commented 1 year ago

It seems that mongoose-zod does not respect the z.nullable() (https://zod.dev/?id=nullables) When I use this schema:

z.object({
  someParam: z.nullable(z.string()),
})

And create a record like this: await SomeModel.create({someParam: null}) I receive the error: ValidationError: SomeModel validation failed: someParam: Path 'someParam' is required.

Alternatively if I change the definition of someParam to: z.string().nullable().optional() and create the record with the same code I get the following error:

ValidationError: SomeModel validation failed: someParam: [
 {
    "code": "invalid_type",
    "expected": "string",
    "received": "null",
    "path": [],
    "message": "Expected string, received null"
 }
]

I think the culprit is this line of code, but I'm not entirely sure. https://github.com/andreww2012/mongoose-zod/blob/main/src/to-mongoose.ts#L88

const isRequired = !schemaFeatures.isOptional && !isZodType(zodSchemaFinal, 'ZodNull');

Could you point out if I'm doing something wrong or what is needed to respect the .nullable() option?

andreww2012 commented 1 year ago

Thank you for the report!

mongoose throws a "parameter is required" error when its value is set to null and the field is required, that's why in mongoose-zod nullish fields are turning in optional fields.

In this particular case, not only I should've marked a nullable field as optional, but also add null to the list of possible values (turn z.string() into something like z.union([z.null(), z.string()]) which, would also make a mongoose schema type become Mixed rather than String).

In other words, a proper null support is difficult. Here, I would probably recommend saving an empty string '' rather than null (or even undefined, i.e. an empty field), is that is possible and makes sense.

MarijnMensinga commented 1 year ago

thanks @andreww2012 for now we made a hacky zod type that fixes this for us.

andreww2012 commented 1 year ago

@MarijnMensinga out of curiosity, mind sharing the "hack"?

MarijnMensinga commented 1 year ago

@andreww2012 sure!

export const optionalPrimitive = <T extends z.ZodTypeAny>(zObject: T) => {
  //TODO: this does not support optional Boolean, must some day be fixed
  return zObject
    .transform((val) => val || undefined)
    .innerType()
    .optional()
}

in conjunction with:

export const SomeZodSchema = z.object({
  ...
  // contextId: z.string().nullable().optional(), 
  contextId: optionalPrimitive(z.string()),
  // dateEnd: z.date().nullable().optional(),
  dateStart: z.date(),
  ...
})
andreww2012 commented 9 months ago

Should be fixed in 0.1.2.