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/swift-kiwis-end.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hono/zod-openapi': patch
---

Reduce route-typing type instantiations (~60% on TS6, ~53% on TS7-rc) by binding expensive sub-expressions once and sharing the input intersection / response-shape conditional.
47 changes: 47 additions & 0 deletions packages/zod-openapi/src/handler.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,53 @@ describe('supports async handler', () => {
hono.openapi(routeWithDefault, errorHandler)
})

test('handler may return a raw Response when no response schema is defined', () => {
// HasZodResponses<R> is false here, so a raw Response is allowed.
const route = createRoute({
method: 'get',
path: '/raw',
responses: {
200: {
description: 'No response schema',
},
},
})

const handler: RouteHandler<typeof route> = () => {
return new Response('hello')
}

const hono = new OpenAPIHono()
hono.openapi(route, handler)
})

test('handler rejects a raw Response when a response schema is defined', () => {
// HasZodResponses<R> is true here, so only the typed response is allowed.
const route = createRoute({
method: 'get',
path: '/typed',
responses: {
200: {
content: {
'application/json': {
schema: z.object({ id: z.string() }),
},
},
description: 'JSON body',
},
},
})

const handler: RouteHandler<typeof route> = (c) => c.json({ id: '123' }, 200)

const hono = new OpenAPIHono()
hono.openapi(route, handler)

// @ts-expect-error a raw Response is not assignable when a response schema is defined
const invalid: RouteHandler<typeof route> = () => new Response('nope')
void invalid
})

test('handler should respect explicitly defined status codes with default fallback', () => {
const routeWithDefault = createRoute({
method: 'get',
Expand Down
72 changes: 72 additions & 0 deletions packages/zod-openapi/src/index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,78 @@ describe('Input types', () => {
})
})

describe('Form, header and cookie input types', () => {
it('Should infer form body input', () => {
const route = createRoute({
method: 'post',
path: '/form',
request: {
body: {
content: {
'application/x-www-form-urlencoded': {
schema: z.object({
name: z.string(),
tag: z.string(),
}),
},
},
},
},
responses: {
200: {
description: 'ok',
},
},
})

const app = new OpenAPIHono()
const routes = app.openapi(route, (c) => {
const form = c.req.valid('form')
assertType<{ name: string; tag: string }>(form)
return c.body(null, 200)
})

type Input = ExtractSchema<typeof routes>['/form']['$post']['input']
type verify = Expect<Equal<Input, { form: { name: string; tag: string } }>>
})

it('Should infer header and cookie input', () => {
const route = createRoute({
method: 'get',
path: '/secure',
request: {
headers: z.object({
authorization: z.string(),
}),
cookies: z.object({
session: z.string(),
}),
},
responses: {
200: {
description: 'ok',
},
},
})

const app = new OpenAPIHono()
const routes = app.openapi(route, (c) => {
const header = c.req.valid('header')
assertType<{ authorization: string }>(header)

const cookie = c.req.valid('cookie')
assertType<{ session: string }>(cookie)

return c.body(null, 200)
})

type Input = ExtractSchema<typeof routes>['/secure']['$get']['input']
type verify = Expect<
Equal<Input, { header: { authorization: string } } & { cookie: { session: string } }>
>
})
})

describe('Response schema includes a Date type', () => {
it('Should not throw a type error', () => {
new OpenAPIHono().openapi(
Expand Down
113 changes: 112 additions & 1 deletion packages/zod-openapi/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Context, TypedResponse } from 'hono'
import { accepts } from 'hono/accepts'
import { bearerAuth } from 'hono/bearer-auth'
import { hc } from 'hono/client'
import type { ServerErrorStatusCode } from 'hono/utils/http-status'
import type { ServerErrorStatusCode, StatusCode } from 'hono/utils/http-status'
import type { JSONValue } from 'hono/utils/types'
import { describe, expect, expectTypeOf, it, vi } from 'vitest'
import { stringify } from 'yaml'
Expand Down Expand Up @@ -2116,6 +2116,117 @@ describe('RouteConfigToTypedResponse', () => {
>
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})

it('Should resolve text/plain responses to the text format', () => {
const route = createRoute({
method: 'get',
path: '/plain',
responses: {
200: {
content: {
'text/plain': {
schema: z.string(),
},
},
description: 'Plain text',
},
},
})

type Actual = RouteConfigToTypedResponse<typeof route>
type Expected = TypedResponse<string, 200, 'text'>
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})

it('Should treat application/*+json media types as json', () => {
const route = createRoute({
method: 'get',
path: '/vnd',
responses: {
200: {
content: {
'application/vnd.api+json': {
schema: UserSchema,
},
},
description: 'Vendor JSON',
},
},
})

type Actual = RouteConfigToTypedResponse<typeof route>
type Expected = TypedResponse<
{
name: string
age: number
hobbies: Record<string, JSONValue>[]
},
200,
'json'
>
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})

it('Should type responses without content as a generic TypedResponse', () => {
const route = createRoute({
method: 'get',
path: '/no-content',
responses: {
204: {
description: 'No Content',
},
},
})

type Actual = RouteConfigToTypedResponse<typeof route>
type Expected = TypedResponse<{}, 204, string>
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})

it('Should resolve the default response to the remaining status codes', () => {
const route = createRoute({
method: 'get',
path: '/with-default',
responses: {
200: {
content: {
'application/json': {
schema: UserSchema,
},
},
description: 'OK',
},
default: {
content: {
'application/json': {
schema: ErrorSchema,
},
},
description: 'Error',
},
},
})

type Actual = RouteConfigToTypedResponse<typeof route>
type Expected =
| TypedResponse<
{
name: string
age: number
hobbies: Record<string, JSONValue>[]
},
200,
'json'
>
| TypedResponse<
{
ok: boolean
},
Exclude<StatusCode, 200>,
'json'
>
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})
})

describe('Generate YAML', () => {
Expand Down
Loading
Loading