diff --git a/.changeset/serious-moose-push.md b/.changeset/serious-moose-push.md new file mode 100644 index 00000000..44e56eb3 --- /dev/null +++ b/.changeset/serious-moose-push.md @@ -0,0 +1,5 @@ +--- +'@hono/zod-openapi': minor +--- + +Support other json content-types such as application/vnd.api+json, application/problem+json, etc. diff --git a/packages/zod-openapi/src/index.ts b/packages/zod-openapi/src/index.ts index 3085c3c9..9d3edee2 100644 --- a/packages/zod-openapi/src/index.ts +++ b/packages/zod-openapi/src/index.ts @@ -55,10 +55,12 @@ type RequestTypes = { } type IsJson = T extends string - ? T extends `application/json${infer _Rest}` - ? 'json' + ? T extends `application/${infer Start}json${infer _End}` + ? Start extends '' | `${string}+` | `vnd.${string}+` + ? 'json' + : never + : never : never - : never type IsForm = T extends string ? T extends @@ -385,7 +387,7 @@ export class OpenAPIHono< if (!(schema instanceof ZodType)) { continue } - if (mediaType.startsWith('application/json')) { + if (/^application\/([a-z-\.]+\+)?json/.test(mediaType)) { const validator = zValidator('json', schema, hook as any) validators.push(validator as any) } diff --git a/packages/zod-openapi/test/index.test.ts b/packages/zod-openapi/test/index.test.ts index c8756a9d..f7945d2b 100644 --- a/packages/zod-openapi/test/index.test.ts +++ b/packages/zod-openapi/test/index.test.ts @@ -422,74 +422,157 @@ describe('JSON', () => { }) .openapi('Post') - const route = createRoute({ - method: 'post', - path: '/posts', - request: { - body: { - content: { - 'application/json': { - schema: RequestSchema, + describe('Content-Type application/json', () => { + const route = createRoute({ + method: 'post', + path: '/posts', + request: { + body: { + content: { + 'application/json': { + schema: RequestSchema, + }, }, }, }, - }, - responses: { - 200: { - content: { - 'application/json': { - schema: PostSchema, + responses: { + 200: { + content: { + 'application/json': { + schema: PostSchema, + }, }, + description: 'Post a post', }, - description: 'Post a post', }, - }, - }) - - const app = new OpenAPIHono() - - app.openapi(route, (c) => { - const { id, title } = c.req.valid('json') - return c.json({ - id, - title, }) - }) - it('Should return 200 response with correct contents', async () => { - const req = new Request('http://localhost/posts', { - method: 'POST', - body: JSON.stringify({ + const app = new OpenAPIHono() + + app.openapi(route, (c) => { + const {id, title} = c.req.valid('json') + return c.json({ + id, + title, + }) + }) + + it('Should return 200 response with correct contents', async () => { + const req = new Request('http://localhost/posts', { + method: 'POST', + body: JSON.stringify({ + id: 123, + title: 'Good title', + }), + headers: { + 'Content-Type': 'application/json', + }, + }) + + const res = await app.request(req) + + expect(res.status).toBe(200) + expect(await res.json()).toEqual({ id: 123, title: 'Good title', - }), - headers: { - 'Content-Type': 'application/json', - }, + }) }) - const res = await app.request(req) - - expect(res.status).toBe(200) - expect(await res.json()).toEqual({ - id: 123, - title: 'Good title', + it('Should return 400 response with correct contents', async () => { + const req = new Request('http://localhost/posts', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({}), + }) + const res = await app.request(req) + expect(res.status).toBe(400) }) }) - it('Should return 400 response with correct contents', async () => { - const req = new Request('http://localhost/posts', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', + describe('Content-Type application/vnd.api+json', () => { + const route = createRoute({ + method: 'post', + path: '/posts', + request: { + body: { + content: { + 'application/vnd.api+json': { + schema: RequestSchema, + }, + }, + }, + }, + responses: { + 200: { + content: { + 'application/json': { + schema: PostSchema, + }, + }, + description: 'Post a post', + }, }, - body: JSON.stringify({}), }) - const res = await app.request(req) - expect(res.status).toBe(400) + + const app = new OpenAPIHono() + + app.openapi(route, (c) => { + const {id, title} = c.req.valid('json') + return c.json({ + id, + title, + }) + }) + + it('Should return 200 response with correct contents', async () => { + const req = new Request('http://localhost/posts', { + method: 'POST', + body: JSON.stringify({ + id: 123, + title: 'Good title', + }), + headers: { + 'Content-Type': 'application/vnd.api+json', + }, + }) + + const res = await app.request(req) + + expect(res.status).toBe(200) + expect(await res.json()).toEqual({ + id: 123, + title: 'Good title', + }) + }) + + it('Should return 400 response with correct contents for empty request data', async () => { + const req = new Request('http://localhost/posts', { + method: 'POST', + headers: { + 'Content-Type': 'application/vnd.api+json', + }, + body: JSON.stringify({}), + }) + const res = await app.request(req) + expect(res.status).toBe(400) + }) + + it('Should return 400 response with correct contents for non application/vnd.api+json request', async () => { + const req = new Request('http://localhost/posts', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({}), + }) + const res = await app.request(req) + expect(res.status).toBe(400) + }) }) }) - +// application/vnd.api+json describe('Form', () => { const RequestSchema = z.object({ id: z.string().openapi({}),