lukeautry / tsoa

Build OpenAPI-compliant REST APIs using TypeScript and Node
MIT License
3.53k stars 499 forks source link

Support zod's infer type #1256

Open cgibson-swyftx opened 2 years ago

cgibson-swyftx commented 2 years ago

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

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

github-actions[bot] commented 2 years ago

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] commented 2 years ago

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

cgibson-swyftx commented 2 years ago

Bump

github-actions[bot] commented 2 years ago

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

cgibson-swyftx commented 2 years ago

Bump

dhad1992 commented 2 years ago

bump

WoH commented 2 years ago

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

therealpaulgg commented 2 years ago

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

tuchk4 commented 2 years ago

faced the same issue

direisc commented 2 years ago

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.

khuongtp commented 1 year ago

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

WoH commented 1 year ago

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.

direisc commented 1 year ago

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.

WoH commented 1 year ago

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

ionmoraru-toptal commented 1 year ago
export type UserParsed = ReturnType<typeof userSchema.parse>

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

matheus-giordani commented 1 year ago

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

i have same problem! Any solution?

WoH commented 1 year ago

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

matheus-giordani commented 1 year ago

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

direisc commented 1 year ago

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.

carlnc commented 1 year ago

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

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.

alexkubica commented 1 year ago

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

developomp commented 1 year ago

@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.

lounging-lizard commented 1 year ago

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

daweimau commented 11 months ago

export type UserParsed = ReturnType

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;
    }
}
WoH commented 11 months ago

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

bompi88 commented 10 months ago

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

frankforpresident commented 10 months ago

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();
    }
  };
}
boian-ivanov commented 10 months ago
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

ashe0047 commented 9 months ago

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

WoH commented 9 months ago

Are you interested in adding support for this?

ashe0047 commented 9 months ago

Are you interested in adding support for this?

Unfortunately I dont really have the time and experience ;-;

boian-ivanov commented 9 months ago

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

This is just not helping the situation @ashe0047

Are you interested in adding support for this?

I am actually, as I really like the idea that TSOA provides, and as I just moved all of our routes/controllers to 5.1.1 and v6 came out. @WoH Do you have any pointers where the issue might be lying? As the interesting thing is that with the ReturnType<...> fix things were compiling properly on v5.1.1, so with v6 something has changed in that area πŸ€” Can't promise that I can fix it, but I'll definitely have a look

nlapointe-swood commented 8 months ago

Any update on it or for the workaround ReturnType? @boian-ivanov maybe you found something?

WoH commented 8 months ago

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

This is just not helping the situation @ashe0047

Are you interested in adding support for this?

I am actually, as I really like the idea that TSOA provides, and as I just moved all of our routes/controllers to 5.1.1 and v6 came out. @WoH Do you have any pointers where the issue might be lying? As the interesting thing is that with the ReturnType<...> fix things were compiling properly on v5.1.1, so with v6 something has changed in that area πŸ€” Can't promise that I can fix it, but I'll definitely have a look

Yeah, we should do something very similar to this, and maybe take that as a good reason to move the logic into a type inference based resolution for this and possibly other types that we resolve using the type checker:

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

Basically, instead of jumping around in the AST, ask TS to give us the type and then use flags and helpers to get what we need.

david-loe commented 7 months ago

Same issue when using mongoose InferSchemaType<>

Generate routes error.
  GenerateMetadataError: No matching model found for referenced type settingsSchema.
     at TypeResolver.getModelTypeDeclarations (/app/node_modules/@tsoa/cli/dist/metadataGeneration/typeResolver.js:1135:19)
     ...
kamit-transient commented 7 months ago

I had similar issues as reported by others and for tsoa 6.1.x zod.infer type does not work at all.

After series of r&d here is my finding:

For. folks who are trying to use zod or zod inferred types or similar solutions here is the workaround.

Since Tsoa already uses decorators so, it would make sense to use class-validator and class-transformer combination to replace Zod completely. And this is what finally working for me.

KKonstantinov commented 6 months ago

Bump. Also tried ReturnType<> and not working on 6.1.x

Mikadore commented 2 months ago

Hi, I would love for this to work, is there something I can do to help?

longzheng commented 1 month ago

I also ran into this issue when returning a response based on a zod schema. On v6.4.0 I seem to be able to workaround it if I move the type into a service layer. For example

For example instead of

// schema.ts
import { z } from "zod";

export const userSchema = z.object({
  name: z.string(),
});

export type User = z.infer<typeof userSchema>;
// testController.ts
import { Get, Controller, Route } from "tsoa";
import { User } from "./schema";

@Route("test")
export class TestController extends Controller {
  @Get("/")
  public getUser(): User {
    return {
      name: "test",
    };
  }
}

Demo sandbox https://codesandbox.io/p/devbox/new-waterfall-3t9s9w?workspaceId=28b5e101-3593-4ce2-888e-ecba9c7ec239 run npm run build in terminal

I have

// schema.ts
import { z } from "zod";

export const userSchema = z.object({
  name: z.string(),
});

export type User = z.infer<typeof userSchema>;
// testService.ts
import { User } from "./schema";

export function getUser(): User {
  return {
    name: "test",
  };
}
// testController.ts
import { Get, Controller, Route } from "tsoa";
import { getUser } from "./testService";

@Route("test")
export class TestController extends Controller {
  @Get("/")
  public getUser() {
    return getUser();
  }
}

Demo sandbox https://codesandbox.io/p/devbox/sharp-hooks-9lcgm7?workspaceId=28b5e101-3593-4ce2-888e-ecba9c7ec239 run npm run build in terminal

iffa commented 3 weeks ago

@longzheng you aren't returning anything in your second example, so of course it works since tsoa isn't inferring anything

longzheng commented 3 weeks ago

@longzheng you aren't returning anything in your second example, so of course it works since tsoa isn't inferring anything

Ah sorry that's a typo. I've updated the sandbox now and it still works.

@Route("test")
export class TestController extends Controller {
  @Get("/")
  public getUser() {
    return getUser();
  }
}

The generated swagger.json has

    "paths": {
        "/test": {
            "get": {
                "operationId": "GetUser",
                "responses": {
                    "200": {
                        "description": "Ok",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "properties": {
                                        "name": {
                                            "type": "string"
                                        }
                                    },
                                    "type": "object"
                                }
                            }
                        }
                    }
                },
                "security": [],
                "parameters": []
            }
        }
    },

I'm currently using this workaround in my own project extensively.

troncoso commented 3 weeks ago

Edit: this actually doesn't do much for me since all the validation is lost when inferring the type. I'm instead going to attempt using the zod-to-openapi library.

I can confirm @longzheng's work around. For some controller endpoints, though, you need a model as a parameter. This is how I got around that:

// models.ts
const orgSchema = z.object({
  id: z.string(),
  name: z.string(),
  foundedYear: z.number(),
});

export type Org = z.infer<typeof orgSchema>;

// service.ts
import { Org } from './models.ts';

export class OrgService {
  ...
  createOrganization(organization: Org): void {
    // do create
  }
}

// controller.ts
import { OrgService } from './service.ts'

// Use the service's param type instead of the model directly
type OrgParam = Parameters<typeof OrgService.prototype.createOrganization>[0];

@Route('orgs')
export class OrgController {
  constructor(private orgService: OrgService) {}

  @Post()
  public async create(@Body() org: OrgParam): Promise<void> {
    this.orgService.createOrganization(org);
  }
}

Not a great solution as you'd have to do that for every parameter you need for every service function. A full proper solution to this ticket would still be great.

nlapointe-dev commented 2 weeks ago

Many thanks @longzheng and @troncoso !!! Works perfectly

longzheng commented 1 week ago

I came up with an even easier workaround that doesn't require a separate service layer by using extends.

// schema.ts
import { z } from "zod";

export const userSchema = z.object({
  name: z.string(),
});

export type User = z.infer<typeof userSchema>;
// testController.ts
import { Get, Controller, Route } from "tsoa";
import { User } from "./schema";

@Route("test")
export class TestController extends Controller {
  @Get("/")
  public getUser(): User extends unknown ? User : never {
    return {
      name: "test",
    };
  }
}

The output swagger.json is

    "paths": {
        "/test": {
            "get": {
                "operationId": "GetUser",
                "responses": {
                    "200": {
                        "description": "Ok",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "properties": {
                                        "name": {
                                            "type": "string"
                                        }
                                    },
                                    "type": "object"
                                }
                            }

Demo sandbox https://codesandbox.io/p/devbox/intelligent-paper-2fch3m?workspaceId=28b5e101-3593-4ce2-888e-ecba9c7ec239

t18n commented 1 day ago

Same story with any other types from inference, for example Drizzle's InferSelectModel.

export const profiles = pgTable(
  'profiles',
  {
    id: integer().primaryKey().generatedAlwaysAsIdentity(),
    headline: varchar('headline', { length: 255 }).notNull(),
    firstName: varchar('first_name', { length: 100 }).notNull(),
  },
);

export type Profile = InferSelectModel<typeof profiles>;

  public async createProfile(): Promise<Profile> {}

This would throw [1] There was a problem resolving type of 'Profile'. [1] Generate routes error. [1] GenerateMetadataError: No matching model found for referenced type profiles. error.

However, if I update the code to

export type ProfileInference = InferSelectModel<typeof profiles>;
export type Profile = Required<ProfileInference>;

Tsoa will generate 2 schemas:

Required_ProfileInference_
Profile

Or tried with Omit, Tsoa generated

Pick_ProfileInference.Exclude_keyofProfileInference.id__
Omit_ProfileInference.id_
Profile

Then I tested it with Zod with the hope that it will work

const profileSchema2 = z.object({
  id: z.number(),
  headline: z.string(),
  summary: z.string(),
  firstName: z.string(),
  lastName: z.string(),
  profilePicture: z.string(),
  userId: z.number()
});

// export type Profile = Omit<ProfileInference, 'id'>;
export type ProfileTest = z.infer<typeof profileSchema2>;
export type Profile = Omit<ProfileTest, 'id'>;

but it didn't work correctly, although console throw no errors image