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

[FEATURE] [v7] Support schema nested objects validation #51

Closed
2 tasks done
alexambrinos opened this issue Jan 12, 2024 · 11 comments
Closed
2 tasks done

[FEATURE] [v7] Support schema nested objects validation #51

alexambrinos opened this issue Jan 12, 2024 · 11 comments
Labels
enhancement New feature or request
Milestone

Comments

@alexambrinos
Copy link

alexambrinos commented Jan 12, 2024

Are you using the latest version of this library?

  • I verified that the issue exists in the latest next-safe-action release

Is there an existing issue for this?

  • I have searched the existing issues and found nothing that matches

Suggest an idea

I wanted to implement a form where a user enters the customer info and the address info at the same time. I've made a custom zod validation like this:
z.object({address: addressSchema, customer: customerSchema })
I did it this way since I can automatically get the schema from drizzle-orm based on the db.
But the problem i faced when implementing this is that the zod validation breaks since flatten() can't have nested objects. Wouldn't it be better to implement the zod validation errors using the format() function instead so we can have nested objects?
The way I need to implement it currently is using two different actions so each form has it's own validation but I lose the ability to add the customer and address in a db transaction since they will be sepparate.

Additional context

No response

@alexambrinos alexambrinos added the enhancement New feature or request label Jan 12, 2024
@TheEdoRan
Copy link
Owner

Hi @alexambrinos, thank you for opening this issue. I understand the problem and yes, it would be nice to be able to pass multiple schemas into a single input object.

next-safe-action, since version 6, uses TypeSchema under the hood to support a wide range of validation libraries, so it's no longer tied to just Zod, therefore it can't and doesn't use format() or flatten() from Zod to build the validationErrors object.

When I released version 6, I wrote a custom function to build the validationErrors object and keep compatibility with previous versions of the library. This usually works well, but in more complex cases like yours, it breaks and the resulting object is quite ugly, with duplicate error messages for parent and children schema properties.

The only (robust) solution that comes to mind is to return the actual issues array when validation fails. So, for instance:

With a schema like this one:

const schema = z.object({
  productSchema: z.object({
    name: z.string().min(1).max(30),
  }),
  userSchema: z.object({
    username: z.string().min(1).max(20),
  }),
});

Assuming that validation fails for both nested properties, you get back a broken validationErrors object like this:

{
  productSchema: [
    "String must contain at least 1 character(s)"
  ],
  name: [
    "String must contain at least 1 character(s)"
  ],
  userSchema: [
    "String must contain at least 1 character(s)"
  ],
  username: [
    "String must contain at least 1 character(s)"
  ]
}

Instead, if we rely on the "raw" issues array of objects from TypeSchema, we get back something like this:

[
  {
    message: "String must contain at least 1 character(s)",
    path: ["productSchema", "name"]
  },
  {
    message: "String must contain at least 1 character(s)",
    path: ["userSchema", "username"]
  }
]

I honestly think that the second one is much more reliable, since it allows granular control over validation errors, with the only drawback of being a little more verbose. It's also a breaking change though, so it requires some thinking to understand if it's the best approach for this problem, but I'm pretty confident it is.

Please let me know what you think about it. If we all agree that this is the best way to handle validation errors from now on, I'll include these changes in next-safe-action v7. Thank you!

@TheEdoRan
Copy link
Owner

A different approach would be to emulate (recreate) the format() function from Zod: see this. This API works well with forms and with nested objects in schemas. It still is a breaking change though, so it would be included in v7.

@TheEdoRan TheEdoRan changed the title [FEATURE] Change flatten() from zod validation error to format() [FEATURE] Support schema nested objects validation Jan 24, 2024
@theboxer
Copy link
Contributor

I'd vote for emulating format() and return the errors as a nested object

TheEdoRan added a commit that referenced this issue Jan 30, 2024
BREAKING CHANGE: `validationErrors` object now follows the same structure as Zod's
[`format()`](https://zod.dev/ERROR_HANDLING?id=formatting-errors) function.

re #51
@TheEdoRan
Copy link
Owner

The validationErrors object in next-safe-action@next now follows the same structure as Zod's format() function. Please let me know what you think about this solution. It was quite difficult to implement, but hopefully the lib now supports any level of nested objects in schemas, while also being fully type safe.

@alexambrinos
Copy link
Author

It returns the errors nested perfectly! The only problem I saw when trying to implement it is if I console.log(result.validationErrors) it shows the object with the properties, but when i try to access a specific field like console.log(result.validationErrors?.customer?.firstName?._errors) it shows as undefined even though it has an error.

@TheEdoRan
Copy link
Owner

when i try to access a specific field ... it shows as undefined even though it has an error

I cannot reproduce the issue. Can you please provide a link to an example repo/StackBlitz project so I can investigate the problem? Thank you!

@theboxer
Copy link
Contributor

theboxer commented Feb 6, 2024

I managed to reproduce it. If your nested object is optional...

with this schema:

const schema = z.object({
  name: z.string(),
  customer: z.object({
    firstName: z.string(),
  }),
});

It works fine and validationErrors?.customer?.firstName?._errors is typed as string[] | undefined.

But, if I adjust the schema and make customer optional:

const schema = z.object({
  name: z.string(),
  customer: z
    .object({
      firstName: z.string(),
    })
    .optional(),
});

It shows that firstName doesn't exist in customer.

The infered type of validationErrors looks like this:

{
      _errors?: string[] | undefined;
      name?: { _errors?: string[] | undefined } | undefined;
      customer?: { _errors?: string[] | undefined } | undefined;
    }
  | undefined

@alexambrinos
Copy link
Author

Sorry, haven't had the time to get into why that was happening, but I can confirm that my address schema was optional, since the user decides if they want to insert the address now or later.

@theboxer
Copy link
Contributor

theboxer commented Feb 6, 2024

It's caused by the SchemaErrors type, that doesn't expect an optional value

export type SchemaErrors<S> = {
	[K in keyof S]?: S[K] extends object ? Extend<ErrorList & SchemaErrors<S[K]>> : ErrorList;
} & {};

this adjustment seems to fix it:

type SchemaErrorValue<Value> = Value extends object ? Extend<ErrorList & SchemaErrors<Value>> : ErrorList;

type IsNullable<T> = undefined extends T ? true : null extends T ? true : false;

export type SchemaErrors<S> = {
	[K in keyof S]?: IsNullable<S[K]> extends true ? SchemaErrorValue<NonNullable<S[K]>> : SchemaErrorValue<S[K]>;
} & {};

@TheEdoRan
Copy link
Owner

Thank you @theboxer for your findings.

I just tried to declare the SchemaErrors type like this and it appears to be working, without declaring additional util types. I updated the S[K] extends object part to S[K] extends object | null | undefined. Please let me know if it works for you as well. Thanks!

export type SchemaErrors<S> = {
  [K in keyof S]?: S[K] extends object | null | undefined
    ? Extend<ErrorList & SchemaErrors<S[K]>>
    : ErrorList;
} & {};

@TheEdoRan
Copy link
Owner

Should be fixed in v7.0.0-next.6.

@TheEdoRan TheEdoRan changed the title [FEATURE] Support schema nested objects validation [FEATURE] [v7] Support schema nested objects validation Feb 7, 2024
@TheEdoRan TheEdoRan added this to the v7 milestone Feb 8, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants