Open stabildev opened 7 months ago
The issue was here:
createRoute({
path: "/{id}",
method: "get",
request: {
params: z.object({
id: z.number().int().positive(), // <--
}),
},
Apparently params must be of type string. Maybe this could create type error somewhere?
You could use coerce
:
export const PetIdRequestParamSchema = z.object({
petId: z.coerce
.number()
.int()
.openapi({
param: {
name: 'petId',
in: 'path',
},
description: 'The pet identifier.',
example: 123,
}),
});
app.openapi(
createRoute({
method: 'get',
path: '/{petId}',
summary: 'Get a pet',
description: 'Returns a pet.',
tags: ['pet'],
operationId: 'pets/get',
request: {
params: PetIdRequestParamSchema,
},
..
@marceloverdijk
Even coerce
breaks type inference:
What seems to be working as a workaround is this:
params: z.object({
id: z
.string()
.transform((v) => Number.parseInt(v))
.refine((v) => !Number.isNaN(v) && v > 0, { message: "Invalid ID" })
.openapi({ type: "integer" }),
}),
However, the whole type inference is extremely slow. I have to wait 15-30 seconds every time for any kind of IntelliSense so I am basically writing the code blind, hoping it won't error later. Any ideas how to solve this?
I am using
hi @stabildev I must admit I found some issue with my suggested approach as well yesterday evening.
The earlier mentioned code:
export const PetIdRequestParamSchema = z.object({
petId: z.coerce
.number()
.int()
.openapi({
param: {
name: 'petId',
in: 'path',
},
description: 'The pet identifier.',
example: 123,
}),
});
app.openapi(
createRoute({
method: 'get',
path: '/{petId}',
summary: 'Get a pet',
description: 'Returns a pet.',
tags: ['pet'],
operationId: 'pets/get',
request: {
params: PetIdRequestParamSchema,
},
..
seems correct:
however the generated OpenAPI spec looked like:
"/api/pets/{petId}": {
"get": {
"summary": "Get a pet",
"description": "Returns a pet.",
"tags": [
"pet"
],
"operationId": "pets/get",
"parameters": [
{
"schema": {
"type": [
"integer",
"null"
],
"description": "The pet identifier.",
"example": 123
},
"required": true,
"in": "path",
"name": "petId"
}
],
note the the type being ["integer", "null"]
which is incorrect as it is required and should not be null
.
I solved it with:
export const PetIdRequestParamSchema = z
.object({
petId: z
.preprocess((val) => {
if (typeof val === 'string') {
const num = Number(val);
if (Number.isInteger(num)) {
return num;
} else {
return NaN;
}
}
return val;
}, z.number().int())
.openapi({
param: {
in: 'path',
name: 'petId',
required: true,
},
description: 'The pet identifier.',
example: 123,
}),
})
.strict();
so first I preprocess the received value, I manually convert it to number, and if that's not possible I return a NaN
causing a validation error. It's a bit more code unfortunately but it works and the generated OpenAPI spec is correct as well:
"/api/pets/{petId}": {
"get": {
"summary": "Get a pet",
"description": "Returns a pet.",
"tags": [
"pet"
],
"operationId": "pets/get",
"parameters": [
{
"schema": {
"type": "integer",
"description": "The pet identifier.",
"example": 123
},
"required": true,
"in": "path",
"name": "petId"
}
],
PS: I'm not experiencing any slow type interference.. which IDE are you using?
hi @marceloverdijk
I'm using VS Code. Are you using the Hono client?
My inference chain looks like this (in this example for the Customer
type):
createSelectSchema
from drizzle-zod
.pick
and .extend
in createRoute
app.route("/customers", new OpenAPIHono<Env>().openapi(getCustomersRoute, getCustomersHandler)
export type App = typeof app
which is chained and includes all routers and middlewaresclient = hc<App>("...");
type CustomerResponse = Awaited<ReturnType<typeof client["customers"]["$get"]>
type RawCustomer = ExtractData<CustomerResponse>
(utility type to narrow the response to the one containing non-null data fieldtype Customer = ReturnType<typeof transformCustomer>
where transformCustomer: (in: RawCustomer) => Customer
I'm also using VC Code, but my chain is different. I'm using Prisma.
@stabildev cool we are using almost the same setup! I also have a bit slow type interference - but still usable - and I think its on the radar of the main developer. We split our api in sections, I might help?
export type Workspace = Extract<InferResponseType<(typeof workspaceClient.workspaces)[':idOrSlug']['$get']>, { data: unknown }>['data'];
you can see how we split our api here: https://github.com/cellajs/cella/blob/development/backend/src/server.ts
that said, the type performance I think is an issue. I think we need an typescript performance expert to somehow look into it. Would be interested in sponsoring that partially.
I was struggling with this as well, however I found a different workaround using with pipe
operator form zod
that may help you out:
z.string()
.optional()
.pipe(z.coerce.number().min(0).default(0))
.openapi({ type: "integer" }),
This way you can still basically use your current schemas and zod
goodies inside pipe
, just make sure the initial check is z.string()
.
The caveat is that the RPC client typing is still string
, but on your route it is parsed to number
accordingly.
I'm not sure if this helps with type inference times, but it's worth a try.
What version of Hono are you using?
4.2.5
What runtime/platform is your app running on?
Wrangler
What steps can reproduce the bug?
Using path parameters (e. g.
GET /customers/:id
) with@hono/zod-openapi
breaks type inference for the complete route.This renders the Hono Client unusable with zod-openapi.
The following is a simplified example with only one route. If this is combined with another route, e. g.
GET /customers
(without parameter), type inference will break for this endpoint as well.What is the expected behavior?
Type inference should return the correct response type:
What do you see instead?
The response type is inferred as
any
:Additional information
Working equivalent example without
@hono/zod-openapi
:Result:
The pattern works for routes without path parameters.