middleware icon indicating copy to clipboard operation
middleware copied to clipboard

[@hono/zod-openapi] Json body validation stops working when bundling with esbuild

Open eric-poitras opened this issue 5 months ago • 7 comments

Which middleware has the bug?

@hono/zod-openapi

What version of the middleware?

0.19.6

What version of Hono are you using?

4.7.8

What runtime/platform is your app running on? (with version if possible)

NodeJs 22 + esbuild 0.25.5

What steps can reproduce the bug?

1 - Build a project that uses validation on the Body 2 - Run it using a tsx and everything works correctly. 3 - Bundle it using esbuild -> everything almost works but the json validator no longer register.

esbuild --sourcemap --sources-content=false --bundle --outfile=./dist/index.js --platform=node --target=node22 ./src/index.ts

What is the expected behavior?

Bundled version should works as the unbundled version.

What do you see instead?

Bundled version does not works as the validator is not registered. The req.valid('json') return undefined and the code explode on some following line.

Additional information

Instanceof operator here fails under esbuild as the prototype chain seems to be the broken:

https://github.com/honojs/middleware/blob/main/packages/zod-openapi/src/index.ts#L509

I previously saw that when you have 2 versions of the lib loaded in the same repository but I checked and it's not the case. It seems as far as I see a side effect of the bundling and the way it emulates CommonJS.

Libraries usually use type guards instead of instanceof operator validate type. This usually provide a more robust solution. I will take a look and provide a PR if needed.

eric-poitras avatar Sep 17 '25 02:09 eric-poitras

Hi @eric-poitras

Thank you for raising the issue.

Projects usually use a typeguard instead of instanceof operator validate type. This usually provide more robusts solution. I will take a look and provide a PR if needed.

Makes sense. We'd happy you create a PR.

yusukebe avatar Sep 17 '25 08:09 yusukebe

Hi @eric-poitras

Thank you for the PR. I've merged, but I confirmed this issue again, and I can't reproduce your problem with your instruction. Can you share the minimal project to reproduce it? I want to confirm this is an actual bug and your PR has been fixed.

As a comment https://github.com/honojs/middleware/pull/1465#issuecomment-3306371112, using typeof many times could be slower than instanceof.

yusukebe avatar Sep 18 '25 09:09 yusukebe

Hi @yusukebe,

I confirm, as @shahradelahi says, that the step to reproduce is not as simple as using esbuild. It seems to be related to the way our specific project is structured that cause esbuild to generate artifacts that breaks the prototype chain.

I cannot provide our project structure as is but I will try to prune it out of IP and create a repeat scenario that I can provide you. I keep you posted on that. My intuition is that it's caused by the fact that we have CommonJS and ESM module mixed.

In the mean time, if there is a performance concern, we can still start with an instanceof test and fallback to the typeguard as a secondary test only if necessary.

It's pretty common for javascript libraries to avoid using instanceof as it's a more brittle way to validate instance types. It's not the first time I have seen instanceof breaks for some reasons. Here are some stuff I found while searching for this bug:

  • https://github.com/asteasolutions/zod-to-openapi/blob/be536b7128925842c1d41e7ab4fb10e034e71a6e/src/lib/zod-is-type.ts#L92
  • https://github.com/colinhacks/zod/discussions/720

I will update you if I have more info. In the mean time, don't hesitate to poke me if you have other questions.

eric-poitras avatar Sep 18 '25 18:09 eric-poitras

Hi @eric-poitras,

This sounds like an issue with your project structure rather than a bug in the @hono/zod-openapi middleware. The problem likely occurs because the @hono/zod-openapi package is being bundled. If you created the API as a package in a monorepo and are running it separately in a different app, you may have installed the package as a dev dependency — which can cause esbuild to bundle the API package and its dependencies together.

shahradelahi avatar Sep 18 '25 20:09 shahradelahi

Okay, I just benchmarked isZod vs instanceof, and @eric-poitras accidentally made validation 5x faster.

     name                                               hz     min     max    mean     p75     p99    p995    p999     rme   samples
   · isZod with Zod v3 schema                28,227,855.15  0.0000  0.0884  0.0000  0.0000  0.0001  0.0001  0.0001  ±0.10%  14113928
   · instanceof ZodType with Zod v3 schema    5,651,887.54  0.0001  0.0231  0.0002  0.0002  0.0002  0.0002  0.0003  ±0.04%   2825944
   · isZod with Zod v4 schema                27,990,436.26  0.0000  0.0835  0.0000  0.0000  0.0000  0.0001  0.0001  ±0.06%  13995219
   · instanceof ZodType with Zod v4 schema    5,553,443.21  0.0002  0.0551  0.0002  0.0002  0.0002  0.0002  0.0003  ±0.05%   2776722
   · isZod with non-zod object               28,012,948.00  0.0000  0.0382  0.0000  0.0000  0.0000  0.0001  0.0001  ±0.04%  14006474
   · instanceof ZodType with non-zod object   5,474,059.99  0.0002  0.0612  0.0002  0.0002  0.0002  0.0002  0.0003  ±0.12%   2737031

Benchmark script
import { bench, describe } from 'vitest'
import { ZodType, z } from 'zod'
import { z as z3 } from 'zod/v3'
import { isZod } from './zod-typeguard'

describe('isZod vs instanceof', () => {
  const v3Schema = z3.string()
  const v4Schema = z.string()
  const nonZodObject = {}

  bench('isZod with Zod v3 schema', () => {
    isZod(v3Schema)
  })

  bench('instanceof ZodType with Zod v3 schema', () => {
    v3Schema instanceof ZodType
  })

  bench('isZod with Zod v4 schema', () => {
    isZod(v4Schema)
  })

  bench('instanceof ZodType with Zod v4 schema', () => {
    v4Schema instanceof ZodType
  })

  bench('isZod with non-zod object', () => {
    isZod(nonZodObject)
  })

  bench('instanceof ZodType with non-zod object', () => {
    nonZodObject instanceof ZodType
  })
})

Now I feel dumb for assuming instanceof would be faster than running typeof five times. That’s wild. At this point, I’m seriously reconsidering ever using instanceof again.

shahradelahi avatar Sep 20 '25 08:09 shahradelahi

Okay, I just benchmarked isZod vs instanceof, and @eric-poitras accidentally made validation 5x faster.

Ah, super interesting. My bad that I've never done the benchmark. If so, we don't need to revert the PR.

yusukebe avatar Sep 22 '25 07:09 yusukebe

TIL, Thanks @shahradelahi for the benchmark! Yet another reason not to use instanceof :) But the main reason is still because it's brittle since it depends on a pristine prototype chain. I have been bitten more than once in that kind of bugs and they can be really unexpected and hard to debug.

To follow up on your comment above, I cannot denies our project structure is a bit convoluted right now but I don't think we're doing anything that is fundamentally wrong: we factored in a library a lot of startup logic to have a consistent behavior across a bunch of services. But this somehow triggered ESBuild to wrap things up in a structure that cause the problem. I am attempting to rebuilding a mock of the structure in a side project to have a minimal step to reproduce but I am not successfull yet and will continue on my spare time this week.

eric-poitras avatar Sep 22 '25 12:09 eric-poitras