Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

partial() doesn't work #798

Closed
nemanjam opened this issue Nov 29, 2021 · 6 comments
Closed

partial() doesn't work #798

nemanjam opened this issue Nov 29, 2021 · 6 comments

Comments

@nemanjam
Copy link

I use userUpdateSchema to make all fields optional and pass undefined password field but keep getting Should be at least 3 characters for password field. The schemas are bellow:

export const userLoginSchema = z.object({
  email: z.string().email(),
  password: z.string().min(3).max(20), // I send undefined for this field
});

export const userRegisterSchema = userLoginSchema.extend({
  name: z.string().min(3).max(15),
  username: z.string().min(3).max(15),
});

export const userUpdateSchema = userRegisterSchema
  .extend({
    avatar: isBrowser() ? z.instanceof(FileList) : z.any(),
  })
  .omit({ email: true })
  .partial(); // make all fields optional

...

const Settings: React.FC<Props> = ({ user }) => {

  const { register, handleSubmit, formState, watch, getValues } = useForm({
    resolver: zodResolver(userUpdateSchema), // use this schema
    defaultValues: {
      username: user.username,
      name: user.name,
      avatar: user.image,
      password: undefined, // like this, just submit initial values
    },
  });
@scotttrinh
Copy link
Collaborator

I'm not familiar with this hook, but are you certain it's not trying to pass "" for password rather than undefined during the validation phase?

From our perspective, your code does work as intended:

CodeSandbox

import test from "tape";
import { z } from "zod";

export const userLoginSchema = z.object({
  email: z.string().email(),
  password: z.string().min(3).max(20)
});

export const userRegisterSchema = userLoginSchema.extend({
  name: z.string().min(3).max(15),
  username: z.string().min(3).max(15)
});

export const userUpdateSchema = userRegisterSchema
  .extend({
    avatar: z.any()
  })
  .omit({ email: true })
  .partial();

test("valid", (t) => {
  t.true(userUpdateSchema.safeParse({}).success); // this succeeds
  t.end();
});

test("invalid", (t) => {
  t.false(userUpdateSchema.safeParse({ password: "" }).success); // this fails
  t.end();
});

Closing as this appears to be more of an issue with integrating Zod into your form validation tool than any problem with Zod itself. Feel free to follow up here if you get something working or have any other questions that we might be able to help with from our side.

@nemanjam
Copy link
Author

In React text fields are never bound to undefined. In #310 I saw workaround for .optional() with .or(z.literal('')) but what about partial()? Is this library usable with React at all?

@scotttrinh
Copy link
Collaborator

@nemanjam

Zod is usable with React, but I think it takes some understanding of how it (and other runtime type systems like it) work. For forms, the concept of "required" or "optional" typically refers to having a non-empty value, where TypeScript considers "" to be a string and thus qualifies as a required value, as does Zod.

For your use case, without knowing much about useForm (which is from React Hook Form, not part of React), I can only give a little guidance, but I'm not a user of that particular library. If your schema needs to allow for the empty string, we don't currently have a shortcut for allowing "" for any key in an object similar to partial. Looking at the docs for React Hook Form, I think you'll need to manually apply .or(z.literal("")) to each key to allow them to be submitted with an empty string. You could also do a little bit of preprocessing to convert any empty strings into undefined so you can use optional:

Updated the CodeSandbox as well

const emptyStringToUndefined = (data: unknown) =>
  Object.entries(data as Record<string, string>).reduce(
    (acc, [key, value]) => ({
      ...acc,
      [key]: value === "" ? undefined : value
    }),
    {}
  );

export const userUpdateSchema = z.preprocess(
  emptyStringToUndefined,
  userRegisterSchema.extend({ avatar: z.any() }).omit({ email: true }).partial()
);

@nemanjam
Copy link
Author

What you wrote makes sense, but it is also relevant that library has elegant api for very common use case such is usage with the most popular React form library because a lot of people will try to use it for such purpose and have hard time with it. Maybe optional() and partial() could accept argument like optional([undefined, null, '']).

@Sti2nd
Copy link

Sti2nd commented Dec 10, 2021

To people who use react-hook-form. Adding .or(z.literal('')) only works for strings.
There is a solution that works for all data types which I described here: react-hook-form/react-hook-form#6980 (comment)

@scotttrinh
Copy link
Collaborator

scotttrinh commented Dec 10, 2021

@nemanjam

it is also relevant that library has elegant api for very common use case such is usage with the most popular React form library because a lot of people will try to use it for such purpose and have hard time with it.

I absolutely agree that easing integration with things like react-hook-form is very important, but we have to be careful about what zod provides to ensure that it is:

  1. Close to the stated goal of being a runtime representation of TypeScript's type system
  2. Flexible for use in as many contexts and use cases as we can

To that end optional is specifically intended to match TypeScript's concept of "optional".

Up to this point, we've really put the requirement on the library to find ways to integrate with zod, but I hope that we can be more proactive and helpful with troubleshooting and contributing to these projects. I don't think there is much zod "core" needs to do, but there is a layer of abstraction between zod the runtime type-system and validation layers for things like forms, JSON, query strings, CSV parsing, etc. and I think we can try to be more actively helpful in those areas.

Having said that, for now, this is a problem that is best served by addressing with the react-hook-form team. I'm happy to help and contribute to that project to help smooth over bumps like this!

Maybe optional() and partial() could accept argument like optional([undefined, null, '']).

Other than partial, your suggestion is exactly what z.union does, so if that works for your purposes, feel free to make yourself a little utility schema:

export const stringyNillable = z.union([z.undefined(), z.null(), z.literal(''), z.string()]);

stringyNillable.parse(undefined); // passes
stringyNillable.parse(null); // passes
stringyNillable.parse(""); // passes
stringyNillable.parse("a non empty string"); // passes

You could make this a bit more generic and use it to create a partial helper, too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants