tsoa icon indicating copy to clipboard operation
tsoa copied to clipboard

Support zod's infer type

Open cgibson-swyftx opened this issue 2 years ago • 45 comments

When using a Zod validator and then passing it to TSOA, it throws this error: Error: No matching model found for referenced type infer.

Types File

export const MyValidator = z.object({
  result: z.object({
    price: z.string().nonempty()
  }),
  code: z.number(),
  msg: z.string().nonempty()
})
export type MyResponse = z.infer<typeof MyValidator>

Then use it in TSO

 @Get()
  public async getRequest (): Promise<MyResponse> {
 

Sorting

  • I'm submitting a ...

    • [ ] bug report
    • [x] feature request
    • [ ] support request
  • I confirm that I

    • [X] used the search to make sure that a similar issue hasn't already been submit

Expected Behavior

When running yarn tsoa spec-and-routes I expect TSOA to be able to use the inferred type generated by Zod.

Current Behavior

It crashes with

Generate routes error.
 Error: No matching model found for referenced type infer.
    at new GenerateMetadataError (/Users/caseygibson/Documents/Github/node_modules/@tsoa/cli/dist/metadataGeneration/exceptions.js:22:28)

Context (Environment)

Version of the library: "^3.14.1" Version of NodeJS: v14.17.4

  • Confirm you were using yarn not npm: [X]

cgibson-swyftx avatar May 26 '22 23:05 cgibson-swyftx

Hello there cgibson-swyftx 👋

Thank you for opening your very first issue in this project.

We will try to get back to you as soon as we can.👀

github-actions[bot] avatar May 26 '22 23:05 github-actions[bot]

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days

github-actions[bot] avatar Jun 26 '22 00:06 github-actions[bot]

Bump

cgibson-swyftx avatar Jun 27 '22 03:06 cgibson-swyftx

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days

github-actions[bot] avatar Jul 29 '22 00:07 github-actions[bot]

Bump

cgibson-swyftx avatar Jul 29 '22 00:07 cgibson-swyftx

bump

dhad1992 avatar Aug 01 '22 00:08 dhad1992

I've labeled this to avoid the bot. Please vote instead of commenting, and feel free to open a PR to fix this.

WoH avatar Aug 01 '22 11:08 WoH

I have the same problem when using yup's inferred types.

therealpaulgg avatar Sep 10 '22 18:09 therealpaulgg

faced the same issue

tuchk4 avatar Sep 22 '22 16:09 tuchk4

I've labeled this to avoid the bot. Please vote instead of commenting, and feel free to open a PR to fix this.

@WoH or other person, can gives me a little help. I really like trying to solve this but I need a little help to understand where I need to looking for.

direisc avatar Oct 05 '22 17:10 direisc

I tried this and it worked (with yup) https://github.com/jquense/yup/issues/946#issuecomment-647051195

khuongtp avatar Nov 03 '22 03:11 khuongtp

I've labeled this to avoid the bot. Please vote instead of commenting, and feel free to open a PR to fix this.

@WoH or other person, can gives me a little help. I really like trying to solve this but I need a little help to understand where I need to looking for.

I wish I could easily help you out here, but given the things you infer, the most likely answer is: Ask the type checker what that type is since you don't wanna work on the AST itself. In code, maybe you can take a look at how we try to resolve Conditional Types, this will likely work similarly.

https://github.com/lukeautry/tsoa/blob/master/packages/cli/src/metadataGeneration/typeResolver.ts#L212

You should be able to extract a lot of that code and reuse it.

WoH avatar Nov 03 '22 09:11 WoH

I'll try to solve infer, input and output from Zod when having more time.

I found a trick to solve partial for output and infer types.

// One schema with zod
const userSchema = z.object({
  username: z.string(),
  // ... what you need to exist
})

export type UserParsed = ReturnType<typeof userSchema.parse>

// use type to define body like that:
@Post()
@Middlewares<RequestHandler>(schemaValidation(userSchema))
public async create(
    @Body() body: UserParsed
) {
  // TODO code here
}

That working on my tests... interesting.

Documentation show really weird but with correct schema information.

direisc avatar Dec 01 '22 14:12 direisc

I assume because ReturnType is a type reference to a type that we already try to resolve via the mechanism I described.

WoH avatar Dec 01 '22 14:12 WoH

export type UserParsed = ReturnType<typeof userSchema.parse>

it created the type, but unfortunately, the validation doesn't work, I assume it' relate to #1067

ionmoraru-toptal avatar Dec 27 '22 18:12 ionmoraru-toptal

I'll try to solve infer, input and output from Zod when having more time.

I found a trick to solve partial for output and infer types.

// One schema with zod
const userSchema = z.object({
  username: z.string(),
  // ... what you need to exist
})

export type UserParsed = ReturnType<typeof userSchema.parse>

// use type to define body like that:
@Post()
@Middlewares<RequestHandler>(schemaValidation(userSchema))
public async create(
    @Body() body: UserParsed
) {
  // TODO code here
}

That working on my tests... interesting.

Documentation show really weird but with correct schema information.

what would it be schemaValidation(...)?

matheus-giordani avatar Jan 04 '23 17:01 matheus-giordani

i have same problem! Any solution?

matheus-giordani avatar Jan 04 '23 17:01 matheus-giordani

See above for a possible workaround, please submit a PR that properly fixes this otherwise :)

WoH avatar Jan 04 '23 17:01 WoH

I realized that when I use this solution, the generated documentation does not recognize the lack of fields even though they are not optional

matheus-giordani avatar Jan 04 '23 17:01 matheus-giordani

I'll try to solve infer, input and output from Zod when having more time. I found a trick to solve partial for output and infer types.

// One schema with zod
const userSchema = z.object({
  username: z.string(),
  // ... what you need to exist
})

export type UserParsed = ReturnType<typeof userSchema.parse>

// use type to define body like that:
@Post()
@Middlewares<RequestHandler>(schemaValidation(userSchema))
public async create(
    @Body() body: UserParsed
) {
  // TODO code here
}

That working on my tests... interesting. Documentation show really weird but with correct schema information.

what would it be schemaValidation(...)?

schemaValidation is a middleware not related with the issue, middleware to validate body with zod schemas.

direisc avatar Jan 24 '23 11:01 direisc

Upon some shallow investigation, I'm able to replicate the underlying problem (without zod in the loop).

In the zod types they have.

export declare abstract class ZodType<Output = any, Def extends ZodTypeDef = ZodTypeDef, Input = Output> {

and it appears that @tsoa/cli/src/metadataGeneration can't handle the generics part (sort of).

I can replicate the similar error with

import { Get, Route } from 'tsoa';

@Route('/')
export class Controller {
    @Get('/zodTest')
    public async zodTest(): Promise<TheType> {
        return '';
    }
}

declare abstract class ObjBase<T = any> {
    readonly _type: T;
}

class ObjWithString extends ObjBase<string> {}

type TheType = ObjWithString['_type'];

Note that

  • If the return type is ObjWithString['_type'] TSOA fails.
  • If the return type is ObjWithString it works.

I noticed that TSOA's AST processing code does not know how to handle a <Output, node, so the type T does not get created/stored.

So later when trying to dive into ObjWithString['...'] you end up with No matching model found for referenced type T.

carlnc avatar Feb 03 '23 00:02 carlnc

this was the solution for me: https://github.com/lukeautry/tsoa/issues/1256#issuecomment-1333814661 Thanks @direisc 😁

alexkubica avatar Mar 27 '23 08:03 alexkubica

@WoH I know this may be inappropriate, but may I politely request you to take a look into this issue again?

This comment (https://github.com/lukeautry/tsoa/issues/1256#issuecomment-1414545885) seems like a good starting point.

developomp avatar Jun 23 '23 14:06 developomp

Bump this, would like to see a solution that maintains validation or mention in the documentation the limitations of the framework.

lounging-lizard avatar Aug 08 '23 12:08 lounging-lizard

export type UserParsed = ReturnType<typeof userSchema.parse>

For me, the equivalent approach in Yup does create the correct type.

But tsoa throws errors unless I use an interface with Queries(), and so this approach seems to fail because it creates a type, not an interface. This seems to be a separate (apparently solved ?) issue, but still blocks this workaround in my case.

Some demos:


With yup

Fails with GenerateMetadataError: @Queries('params') only support 'refObject' or 'nestedObjectLiteral' types. If you want only one query parameter, please use the '@Query' decorator.

import * as yup from "yup";
import { Controller, Get, Queries, Route } from "tsoa";

const basketSchema = yup.object({
    id: yup.number().optional(),
    name: yup.string().optional(),
});

type TGetBasketParams = ReturnType<typeof basketSchema.validateSync>;

@Route("basket")
export class Basket extends Controller {
    @Get()
    public static get(@Queries() params: TGetBasketParams) {
        return;
    }
}

No yup, just a plain old type

Fails with GenerateMetadataError: @Queries('params') only support 'refObject' or 'nestedObjectLiteral' types. If you want only one query parameter, please use the '@Query' decorator.

import { Controller, Get, Queries, Route } from "tsoa";

type TGetBasketParams = {
    id?: number;
    name?: string;
};

@Route("basket")
export class Basket extends Controller {
    @Get()
    public static get(@Queries() params: TGetBasketParams) {
        return;
    }
}

no yup, same type as an interface

Works.

import { Controller, Get, Queries, Route } from "tsoa";

interface TGetBasketParams {
    id?: number;
    name?: string;
}

@Route("basket")
export class Basket extends Controller {
    @Get()
    public static get(@Queries() params: TGetBasketParams) {
        return;
    }
}

daweimau avatar Nov 24 '23 12:11 daweimau

@daweimau Would you mind setting up a repro for this? Not sure I have the time to support 1), but 2) should be reasonable.

WoH avatar Nov 25 '23 00:11 WoH

@WoH What is needed for this feature and how can I help?

bompi88 avatar Dec 21 '23 07:12 bompi88

i have same problem! Any solution?

This is my validation middleware for Yup but I guess with a small refactor you use it for zod as well

import { RequestHandler } from 'express';
import * as Yup from 'yup';

export function schemaValidation(schema: Yup.Schema<any>): RequestHandler {
  return (req, res, next) => {
    try {
      schema.validateSync(req.body, { abortEarly: false });
      next();
    } catch (err: Yup.ValidationError | any) {
      if (err instanceof Yup.ValidationError) {
        log.error(`Caught Yup Validation Error for ${req.path}:`, err.errors);
        return res.status(422).json({
          message: 'Validation Failed',
          details: err?.errors,
        });
      }

      next();
    }
  };
}

frankforpresident avatar Dec 21 '23 08:12 frankforpresident

export type UserParsed = ReturnType<typeof userSchema.parse>

With the newer version 6.0.0, now not even this workaround is valid 😞

Also tried the default z.infer<...> approach and that still gives the same error

boian-ivanov avatar Dec 29 '23 09:12 boian-ivanov

Any solution, its 2023 and this issue is still present. LOL

ashe0047 avatar Jan 01 '24 13:01 ashe0047