Repository navigation
Conversation
tRPC's getParseFn binds parseAsync as soon as a validator exposes it, so every procedure validates on the validator's async path even when no schema in the application has an async refinement. On zod that costs twice to nearly four times the synchronous path depending on payload size, and it puts the compiled fast path of zod 4.5 out of reach, since zod/compile defers to the interpreter on any async context. The new `syncValidation` option, off by default, wraps input and output schemas in a parser that tries the synchronous path first. tRPC accepts a plain function as a first-class parser, so the rest of the pipeline is untouched, and codegen is unaffected because the generator reads the decorators statically rather than the runtime value. The parser assumes as little as possible about the validator, since the peer range covers zod 3 and zod 4: it engages only for a schema exposing both `parse` and `parseAsync`, treats an exception carrying `issues` as a validation verdict and propagates it unchanged, and switches to `parseAsync` on anything else. That switch is remembered per schema, so a schema with an async refinement pays the aborted synchronous attempt once per process rather than running its synchronous callbacks twice on every call.
|
@dlecan is attempting to deploy a commit to the Kevin's Projects Team on Vercel. A member of the Team first needs to authorize it. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Thanks for this — the investigation in #120 is one of the best-measured perf reports this repo has gotten, and the implementation here is careful. I'm going to decline the approach, though, and I want to propose a direction that keeps the win. The library's contract is to wire tRPC into NestJS, with tRPC behaving exactly as it does when wired by hand. A module option that changes which parser path runs breaks that contract, once it's in, this library owns validation-scheduling behavior and its interactions with every future tRPC and zod release. That said, you've surfaced a real gap on our side. A raw tRPC user hits this and writes The fix I'd take a PR for: support function parsers in Happy to scope that in an issue if you'd rather not rework this PR. And if you do open the upstream issue, link it here 🙏 |
|
Opened upstream as trpc/trpc#7626, with the numbers redone on zod 4.5. I'll come back to the function parser proposal separately. |
|
Thanks for pointing in this direction. Checking 2.13 first, function parsers in the decorators already generate and import correctly when the helper is exported, so #123 only covers what was missing: a clear error for an unexported parser function, |
|
Closing in favor of #123 and trpc/trpc#7626 — thanks for carrying this in both directions. I'll review #123 shortly. |
Closes #120.
getParseFnbindsparseAsyncas soon as a validator exposes it, so today everyprocedure validates on the validator's async path even when no schema in the application
has an async refinement. On zod that costs ×2 to ×3.7 depending on payload size, and it
makes zod 4.5's compiled fast path unreachable, since
zod/compiledefers to the interpreter on any async context. Numbers and references are in the issue.What this adds
syncValidationonTRPCModuleOptions, off by default. When enabled,ProcedureFactorywraps input and output schemas in a parser that tries the synchronous path first. tRPC
accepts a plain function as a first-class parser (
ParserCustomValidatorEsque), so therest of the pipeline is untouched, and codegen is unaffected because the generator reads
the decorators statically rather than the runtime value.
lib/utils/sync-first-parser.util.ts: the parser.lib/interfaces/module-options.interface.ts: the option, documented like its neighbours.lib/factories/procedure.factory.ts: injectsTRPC_MODULE_OPTIONSthe wayMiddlewareFactoryalready does, and resolves the parser at the two wiring points.Design notes
The parser assumes as little as possible about the validator, since the peer range covers
zod 3 and zod 4:
parseandparseAsync; anything else isreturned untouched, including plain function parsers and standard-schema-only objects.
issuesis a validation verdict and propagates unchanged. Anythingelse means the validator refused to run synchronously, and the call switches to
parseAsync.would run its synchronous callbacks twice on every single call; with it, that happens
once per process. zod applies the same bound inside its own compiler.
Tests
lib/utils/__tests__/sync-first-parser.util.spec.tscovers the pass-through, thesynchronous path with
parseAsyncspied to prove it is never touched, the validation errorpropagating without an async retry, the fallback for an async refinement, the single
aborted attempt across three calls, and a transforming schema keeping its parsed output.
lib/factories/__tests__/procedure.factory.spec.tsgains asyncValidationblockasserting that schemas reach tRPC untouched when the option is off, that they are wrapped
when it is on, and that the wrapped parser validates like the schema it replaces. The
existing suite required one change:
ProcedureFactorynow injectsTRPC_MODULE_OPTIONS,so the testing module provides it.
Full package suite: 18 files, 87 tests, green. Lint and
tsc --project tsconfig.build.jsonclean.
If you would rather have it on by default
Say so and I will flip it. The opt-in exists only to keep the change free of behavioural
risk for existing users; the fallback makes the default-on version correct too, at the cost
of that one duplicate run per async schema.