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

Validation using zod #1462

Open
valerii15298 opened this issue Jun 1, 2023 · 7 comments
Open

Validation using zod #1462

valerii15298 opened this issue Jun 1, 2023 · 7 comments
Labels
Community 👨‍👧 Something initiated by a community Discussion 💬 Brainstorm about the idea Documentation 📖 Issues about docs Enhancement 🆕 New feature or request

Comments

@valerii15298
Copy link

valerii15298 commented Jun 1, 2023

Is your feature request related to a problem? Please describe.
Since my project extensively uses zod I want to be able validate InputTypes and maybe ObjectTypes too using zod.

Describe the solution you'd like
something like this?

@InputType()
class Bar {
  @ZodValidate(z.string())
  @Field()
  field: string;
}

Describe alternatives you've considered
Using built in class-validator or joiful as described here => https://typegraphql.com/docs/validation.html#custom-validator

Additional context
joiful does not seem to be well maintained. class-validator seems not bad but zod is already quite popular library with friendly and understandable API having high flexibility and customization. So it might be worth to integrate zod with type-graphql.
I feel like it is already possible to do it with zod by using Extension decorator => https://typegraphql.com/docs/extensions.html and custom validation function => https://typegraphql.com/docs/validation.html#custom-validator but I did not find examples in docs how to do that.
Basically how to extract extensions data in custom validation function?
If it is possible, I can even maybe contribute to add examples with zod validation if needed and someone can give me direction of where to start and where to look for...

So what would be the best way to integrate zod with typegraphql?

@carlocorradini
Copy link
Contributor

carlocorradini commented Jun 1, 2023

An example using zod should be added but I don't know if it is possible. @MichalLytek surely have an idea on this 🥳🤗

@MichalLytek
Copy link
Owner

I have no experience with zod so I can't help you.
I only have feelings that zod won't play nice with decorators because it's also "schema declaration" library, so it focuses on declaring the shape of objects, which we already have with TS classes.

@MichalLytek MichalLytek added Enhancement 🆕 New feature or request Community 👨‍👧 Something initiated by a community Documentation 📖 Issues about docs Discussion 💬 Brainstorm about the idea labels Jun 2, 2023
@MichalLytek MichalLytek added this to the Future release milestone Jun 2, 2023
@valerii15298
Copy link
Author

valerii15298 commented Jun 3, 2023

I guess there is a question how much and are we gonna allow for zod to validate. For example this plugin for nest https://github.com/incetarik/nestjs-graphql-zod allows to validate nested objects too.

Agnostic approach for type-graphql could be nice, for example if we can assign some metadata to specific field and then access it in validation function, this will allow integrate any validation library with type-graphql. If we just have access to field metadata then writing zod validation function is quite trivial:

@InputType()
class Bar {
  @Extensions({ zodSchema: z.string() }) // some way to assign metadata to the function
  @Field()
  field: string;
}

const schema = await buildSchema({
  // ...other options
  validate: (argValue, argType, fieldMetadata) => {
    // we just need to access extensions metadata in this validate function
    fieldMetadata?.zodSchema.parse(argValue) // this will throw on validation error
    // the above same as `z.string().parse(argValue)`
  },
});

I do not know anything about how hard is to implement this for type-graphql, it is just as an example idea

@MichalLytek
Copy link
Owner

MichalLytek commented Jun 3, 2023

@Extensions are GraphQL specific. All you need is a generic decorator approach that will work with any framework, just like class-validator. So storing the metadata should be done on, let's name it, zod-decorators package:

@InputType()
class Bar {
  @Zod(z => z.string()) // some way to assign metadata to the function
  @Field()
  field: string;
}

validate: (argValue, argType) => {
  zodDecorators.validate(argType, argValue); // reads validation schema from own storage for `argType` class and parse `argValue` value
},

@angelhodar
Copy link

angelhodar commented Sep 30, 2023

I have just tried to create a custom validator for any args passed to a graphql query or mutation:

import { createMethodDecorator, ArgumentValidationError } from 'type-graphql';
import { ValidationError } from 'class-validator';
import { z } from 'zod';

type SchemaMap = { [argName: string]: z.Schema<any> };

function convertZodErrorToClassValidatorError(zodError: z.ZodError, argName: string): ValidationError[] {
  return zodError.errors.map((error) => {
    const validationError = new ValidationError();
    validationError.property = argName;
    validationError.constraints = { [error.code]: error.message };
    return validationError;
  });
}

export function ZodValidate(schemaMap: SchemaMap) {
  return createMethodDecorator(async ({ args }, next) => {
    for (const argName in schemaMap) {
      const schema = schemaMap[argName];
      const argValue = args[argName];
      const result = schema.safeParse(argValue);
      if (!result.success) {
        const validationErrors = convertZodErrorToClassValidatorError(result.error, argName);
        throw new ArgumentValidationError(validationErrors);
      }
    }
    return next();
  });
}

Usage example:

@InputType()
export class TestInput {
  @Field()
  targetAudience: string;

  @Field()
  annualLaunches: string;

  @Field(() => Float)
  employees: number;
}

....

const schema = z.object({ targetAudience: z.string().max(3), annualLaunches: z.string(), employees: z.number() });

@Mutation(() => Boolean)
  @ZodValidate({ input: schema })
  async zodValidated(@Arg('input') input: TestInput): Promise<boolean> {
    console.log(input);
    return true;
  }

And it validates as expected but the problem is that the output error is not formatted properly (the extensions exception doesnt include any validation errors paased to the ArgumentValidationError constructor):

{
  message: 'Argument Validation Error',
  locations: [ { line: 2, column: 3 } ],
  path: [ 'zodValidated' ],
  extensions: {
    code: 'INTERNAL_SERVER_ERROR',
    stacktrace: [
      'Error: Argument Validation Error',
      '    at C:\\Users\\angel\\...'] // Ommited rest of stacktrace for privacy
  }
}

Any idea why this happens @MichalLytek?

@Alex0007
Copy link

I think that zod is better than class-validator, because zod can do conditional validation, while class-validator is limited in that area by only working with fields mostly

ref: colinhacks/zod#2099 (comment)

my situation: i want to validate GraphQl input objects and fields list: required fields are depending on type property of input type

@PS1TD
Copy link

PS1TD commented Nov 6, 2024

I have just tried to create a custom validator for any args passed to a graphql query or mutation:

import { createMethodDecorator, ArgumentValidationError } from 'type-graphql';
import { ValidationError } from 'class-validator';
import { z } from 'zod';

type SchemaMap = { [argName: string]: z.Schema<any> };

function convertZodErrorToClassValidatorError(zodError: z.ZodError, argName: string): ValidationError[] {
  return zodError.errors.map((error) => {
    const validationError = new ValidationError();
    validationError.property = argName;
    validationError.constraints = { [error.code]: error.message };
    return validationError;
  });
}

export function ZodValidate(schemaMap: SchemaMap) {
  return createMethodDecorator(async ({ args }, next) => {
    for (const argName in schemaMap) {
      const schema = schemaMap[argName];
      const argValue = args[argName];
      const result = schema.safeParse(argValue);
      if (!result.success) {
        const validationErrors = convertZodErrorToClassValidatorError(result.error, argName);
        throw new ArgumentValidationError(validationErrors);
      }
    }
    return next();
  });
}

Usage example:

@InputType()
export class TestInput {
  @Field()
  targetAudience: string;

  @Field()
  annualLaunches: string;

  @Field(() => Float)
  employees: number;
}

....

const schema = z.object({ targetAudience: z.string().max(3), annualLaunches: z.string(), employees: z.number() });

@Mutation(() => Boolean)
  @ZodValidate({ input: schema })
  async zodValidated(@Arg('input') input: TestInput): Promise<boolean> {
    console.log(input);
    return true;
  }

And it validates as expected but the problem is that the output error is not formatted properly (the extensions exception doesnt include any validation errors paased to the ArgumentValidationError constructor):

{
  message: 'Argument Validation Error',
  locations: [ { line: 2, column: 3 } ],
  path: [ 'zodValidated' ],
  extensions: {
    code: 'INTERNAL_SERVER_ERROR',
    stacktrace: [
      'Error: Argument Validation Error',
      '    at C:\\Users\\angel\\...'] // Ommited rest of stacktrace for privacy
  }
}

Any idea why this happens @MichalLytek?

I would like to add on top of this as I am in need of a similar configuration and eventually came to a similar solution.
In my case the zod schemas not only have validation but they have certain mutation features aswell.
Like z.string().toLowecase()
The promblem is i cannot pass the result of zod.parse back into the next() function to be processed by the next middleware

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Community 👨‍👧 Something initiated by a community Discussion 💬 Brainstorm about the idea Documentation 📖 Issues about docs Enhancement 🆕 New feature or request
Projects
None yet
Development

No branches or pull requests

7 participants
@Alex0007 @MichalLytek @carlocorradini @angelhodar @PS1TD @valerii15298 and others