tsoa
tsoa copied to clipboard
Support zod's infer type
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]
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.👀
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
Bump
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
Bump
bump
I've labeled this to avoid the bot. Please vote instead of commenting, and feel free to open a PR to fix this.
I have the same problem when using yup's inferred types.
faced the same issue
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 tried this and it worked (with yup) https://github.com/jquense/yup/issues/946#issuecomment-647051195
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.
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.
I assume because ReturnType is a type reference to a type that we already try to resolve via the mechanism I described.
export type UserParsed = ReturnType<typeof userSchema.parse>
it created the type, but unfortunately, the validation doesn't work, I assume it' relate to #1067
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(...)?
i have same problem! Any solution?
See above for a possible workaround, please submit a PR that properly fixes this otherwise :)
I realized that when I use this solution, the generated documentation does not recognize the lack of fields even though they are not optional
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.
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.
this was the solution for me: https://github.com/lukeautry/tsoa/issues/1256#issuecomment-1333814661 Thanks @direisc 😁
@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.
Bump this, would like to see a solution that maintains validation or mention in the documentation the limitations of the framework.
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 Would you mind setting up a repro for this? Not sure I have the time to support 1), but 2) should be reasonable.
@WoH What is needed for this feature and how can I help?
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();
}
};
}
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
Any solution, its 2023 and this issue is still present. LOL