Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/late-toes-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hono/zod-openapi': minor
---

Added support for zod v4
6 changes: 3 additions & 3 deletions packages/zod-openapi/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"homepage": "https://github.com/honojs/middleware",
"peerDependencies": {
"hono": ">=4.3.6",
"zod": ">=3.0.0"
"zod": "^3.25.0 || ^4.0.0"
},
"devDependencies": {
"@arethetypeswrong/cli": "^0.17.4",
Expand All @@ -51,10 +51,10 @@
"typescript": "^5.8.2",
"vitest": "^3.2.4",
"yaml": "^2.4.3",
"zod": "^3.22.1"
"zod": "^4.0.5"
},
"dependencies": {
"@asteasolutions/zod-to-openapi": "^7.3.0",
"@asteasolutions/zod-to-openapi": "^8",
"@hono/zod-validator": "workspace:^",
"openapi3-ts": "^4.5.0"
},
Expand Down
4 changes: 2 additions & 2 deletions packages/zod-openapi/src/index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,15 +232,15 @@ describe('coerce', () => {
}),
(c) => {
const { id } = c.req.valid('param')
assertType<number>(id)
assertType<unknown>(id)
return c.json({ id })
}
)

type Actual = ExtractSchema<typeof routes>['/api/users/:id']['$get']['input']
type Expected = {
param: {
id: number
id: unknown
}
}
type verify = Expect<Equal<Expected, Actual>>
Expand Down
49 changes: 32 additions & 17 deletions packages/zod-openapi/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
OpenApiGeneratorV3,
OpenApiGeneratorV31,
extendZodWithOpenApi,
getOpenApiMetadata,
} from '@asteasolutions/zod-to-openapi'
import { zValidator } from '@hono/zod-validator'
import { Hono } from 'hono'
Expand Down Expand Up @@ -38,8 +39,19 @@ import type { JSONParsed, JSONValue, RemoveBlankRecord, SimplifyDeepArray } from
import { mergePath } from 'hono/utils/url'
import type { OpenAPIObject } from 'openapi3-ts/oas30'
import type { OpenAPIObject as OpenAPIV31bject } from 'openapi3-ts/oas31'
import type { ZodError, ZodSchema } from 'zod'
import { ZodType, z } from 'zod'
import * as z3 from 'zod/v3'
import * as z4 from 'zod/v4/core'
import * as z from 'zod/v4'

type ZodType = z3.ZodTypeAny | z4.$ZodType
type ZodError<T extends ZodType> = T extends z4.$ZodType ? z4.$ZodError : z3.ZodError
type ZodInput<T> = T extends z3.ZodType ? z3.input<T> : T extends z4.$ZodType ? z4.input<T> : never
type ZodOutput<T> = T extends z3.ZodType
? z3.output<T>
: T extends z4.$ZodType
? z4.output<T>
: never
type ZodInfer<T> = T extends z3.ZodType ? z3.infer<T> : T extends z4.$ZodType ? z4.infer<T> : never

type MaybePromise<T> = Promise<T> | T

Expand Down Expand Up @@ -110,13 +122,13 @@ type InputTypeBase<
in: {
[K in Type]: HasUndefined<ValidationTargets[K]> extends true
? {
[K2 in keyof z.input<RequestPart<R, Part>>]?: z.input<RequestPart<R, Part>>[K2]
[K2 in keyof ZodInput<RequestPart<R, Part>>]?: ZodInput<RequestPart<R, Part>>[K2]
}
: {
[K2 in keyof z.input<RequestPart<R, Part>>]: z.input<RequestPart<R, Part>>[K2]
[K2 in keyof ZodInput<RequestPart<R, Part>>]: ZodInput<RequestPart<R, Part>>[K2]
}
}
out: { [K in Type]: z.output<RequestPart<R, Part>> }
out: { [K in Type]: ZodOutput<RequestPart<R, Part>> }
}
: {}
: {}
Expand All @@ -128,16 +140,16 @@ type InputTypeJson<R extends RouteConfig> = R['request'] extends RequestTypes
? {}
: R['request']['body']['content'][keyof R['request']['body']['content']] extends Record<
'schema',
ZodSchema<any>
ZodType
>
? {
in: {
json: z.input<
json: ZodInput<
R['request']['body']['content'][keyof R['request']['body']['content']]['schema']
>
}
out: {
json: z.output<
json: ZodOutput<
R['request']['body']['content'][keyof R['request']['body']['content']]['schema']
>
}
Expand All @@ -154,16 +166,16 @@ type InputTypeForm<R extends RouteConfig> = R['request'] extends RequestTypes
? {}
: R['request']['body']['content'][keyof R['request']['body']['content']] extends Record<
'schema',
ZodSchema<any>
ZodType
>
? {
in: {
form: z.input<
form: ZodInput<
R['request']['body']['content'][keyof R['request']['body']['content']]['schema']
>
}
out: {
form: z.output<
form: ZodOutput<
R['request']['body']['content'][keyof R['request']['body']['content']]['schema']
>
}
Expand All @@ -181,8 +193,8 @@ type InputTypeCookie<R extends RouteConfig> = InputTypeBase<R, 'cookies', 'cooki
type ExtractContent<T> = T extends {
[K in keyof T]: infer A
}
? A extends Record<'schema', ZodSchema>
? z.infer<A['schema']>
? A extends Record<'schema', ZodType>
? ZodInfer<A['schema']>
: never
: never

Expand Down Expand Up @@ -230,7 +242,7 @@ export type Hook<T, E extends Env, P extends string, R> = (
}
| {
success: false
error: ZodError
error: ZodError<any>
}
),
c: Context<E, P>
Expand Down Expand Up @@ -499,7 +511,7 @@ export class OpenAPIHono<
continue
}
const schema = (bodyContent[mediaType] as ZodMediaTypeObject)['schema']
if (!(schema instanceof ZodType)) {
if (!(schema instanceof z4.$ZodType || schema instanceof z3.ZodType)) {
continue
}
if (isJSONContentType(mediaType)) {
Expand Down Expand Up @@ -659,11 +671,14 @@ export class OpenAPIHono<
}

case 'schema':
return this.openAPIRegistry.register(def.schema._def.openapi._internal.refId, def.schema)
return this.openAPIRegistry.register(
getOpenApiMetadata(def.schema)._internal?.refId,
def.schema
)

case 'parameter':
return this.openAPIRegistry.registerParameter(
def.schema._def.openapi._internal.refId,
getOpenApiMetadata(def.schema)._internal?.refId,
def.schema
)

Expand Down
18 changes: 9 additions & 9 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -99,14 +99,14 @@ __metadata:
languageName: node
linkType: hard

"@asteasolutions/zod-to-openapi@npm:^7.3.0":
version: 7.3.0
resolution: "@asteasolutions/zod-to-openapi@npm:7.3.0"
"@asteasolutions/zod-to-openapi@npm:^8":
version: 8.0.0
resolution: "@asteasolutions/zod-to-openapi@npm:8.0.0"
dependencies:
openapi3-ts: "npm:^4.1.2"
peerDependencies:
zod: ^3.20.2
checksum: 10c0/f0a68a89929cdeaa3e21d2027489689f982824d676a9332c680e119f60881dd39b571324b24ad4837fda49bf6fe7c3e2af2199268b281bf1aec923d7a7cbfc40
zod: ^4.0.0
checksum: 10c0/b522d074832fb137dca724c8bd4bb134c7b4d4cad12c247ed3c864f993923b3475fc06580e6e1cbc4fd8641cd361679bbe1dd87c9bb42e142bc056d96d59fbc8
languageName: node
linkType: hard

Expand Down Expand Up @@ -2734,7 +2734,7 @@ __metadata:
resolution: "@hono/zod-openapi@workspace:packages/zod-openapi"
dependencies:
"@arethetypeswrong/cli": "npm:^0.17.4"
"@asteasolutions/zod-to-openapi": "npm:^7.3.0"
"@asteasolutions/zod-to-openapi": "npm:^8"
"@hono/zod-validator": "workspace:^"
hono: "npm:^4.8.4"
openapi3-ts: "npm:^4.5.0"
Expand All @@ -2743,10 +2743,10 @@ __metadata:
typescript: "npm:^5.8.2"
vitest: "npm:^3.2.4"
yaml: "npm:^2.4.3"
zod: "npm:^3.22.1"
zod: "npm:^4.0.5"
peerDependencies:
hono: ">=4.3.6"
zod: ">=3.0.0"
zod: ^3.25.0 || ^4.0.0
languageName: unknown
linkType: soft

Expand Down Expand Up @@ -16830,7 +16830,7 @@ __metadata:
languageName: node
linkType: hard

"zod@npm:^3.20.2, zod@npm:^3.22.1, zod@npm:^3.22.3":
"zod@npm:^3.20.2, zod@npm:^3.22.3":
version: 3.24.2
resolution: "zod@npm:3.24.2"
checksum: 10c0/c638c7220150847f13ad90635b3e7d0321b36cce36f3fc6050ed960689594c949c326dfe2c6fa87c14b126ee5d370ccdebd6efb304f41ef5557a4aaca2824565
Expand Down