MichalLytek / type-graphql

Create GraphQL schema and resolvers with TypeScript, using classes and decorators!
https://typegraphql.com
MIT License
8.04k stars 677 forks source link

Validation using zod #1462

Open valerii15298 opened 1 year ago

valerii15298 commented 1 year ago

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 commented 1 year ago

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

MichalLytek commented 1 year ago

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.

valerii15298 commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

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 commented 7 months ago

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: https://github.com/colinhacks/zod/discussions/2099#discussioncomment-5101317

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

PS1TD commented 3 weeks ago

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