feat(openapi): modularize generator and hoist error schemas into components#1721
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
More templates
@orpc/ai-sdk
@orpc/arktype
@orpc/bun
@orpc/client
@orpc/cloudflare
@orpc/contract
@orpc/experimental-effect
@orpc/evlog
@orpc/json-schema
@orpc/nest
@orpc/next
@orpc/openapi
@orpc/opentelemetry
@orpc/pinia-colada
@orpc/pino
@orpc/publisher
@orpc/ratelimit
@orpc/server
@orpc/shared
@orpc/tanstack-query
@orpc/trpc
@orpc/valibot
@orpc/zod
commit: |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — rewrites the OpenAPI generator into focused modules, hoists error schemas into components.schemas, and brings every generator module to 100% test coverage.
- Generator modularization —
openapi-generator.tsshrinks to ~190 lines of orchestration, delegating schema hoisting toOpenAPIComponentRegistryand request/response/error building tobuildRequest/buildSuccessResponse/buildErrorResponse. - Error schema hoisting — defined error responses now reference named
$refcomponents derived from error codes (e.g.NOT_FOUND→NotFound), with numbered postfixes on name conflicts and reuse of equal schemas across procedures. visitJsonSchemaRefs— shared traversal in@orpc/json-schemathat walks the same keywords asmapJsonSchemaRefs, preventing ref-discovery drift.- Test restructuring — single 3900-line test file replaced by 14 focused test files plus shared
testSchema/testSchemaConverterhelpers for schema-library-agnostic coverage. - Component reuse fixes — fixed dangling
$defsrefs inpatternProperties/containsand fixed reuse against a previously generated base document.
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — consolidates generator unit tests into the main openapi-generator.test.ts file, adds scenario-driven e2e tests under tests/, and tidies the generator's imports and internal plumbing.
openapi-generator.tsslimmed to ~189 lines — privaterequest/successResponse/errorResponsemethods replaced by calls tobuildRequest/buildSuccessResponse/buildErrorResponse;OpenAPIGeneratorErrorand theOpenAPIOperationContextcontract are now defined inopenapi-generator-operation.tsand re-exported.visitJsonSchemaRefs— new traversal utility in@orpc/json-schemathat mirrorsmapJsonSchemaRefs's keyword set, with shared/cyclic instance dedup. Tested against the same keywordsmapJsonSchemaRefsrewrites.- Test reorganization — separate
*-schema,*-error-response,*-hoisting, and*-request-bodytest files merged intoopenapi-generator.test.ts(18 orchestration tests), while per-module unit tests stay inopenapi-generator-operation.test.ts(35) andopenapi-generator-components.test.ts(30). - E2E tests — 32 new tests across 8 files (
crud,composed-schemas,detailed-http,event-streaming,file-transfer,reusable-components,schema-libraries,typed-errors) written from end-user perspectives. SharedtestSchema/testSchemaConverterhelpers live intests/__shared__/schema.ts.
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — rewrites error messages in the generator operation module to be clearer and more actionable, and fixes double-pushed error messages in the per-procedure error collection.
- Error message rewrites — every error thrown by
buildRequest,buildSuccessResponse, andbuildErrorResponsenow identifies the procedure with a cleaner format (Procedure "path":instead ofProcedure at path "path"), uses plain language, and includes concrete fix suggestions (e.g. "Fix: make the input an object, or use a method with a request body"). - Error aggregation cleanup — the per-procedure error handler now pushes
e.messagedirectly instead of wrapping it with a redundant[OpenAPIGenerator]prefix. The aggregate message includes an error count (Failed to generate the OpenAPI document (N errors)). - Test parity — test assertions updated to match the new message formats, and a new test covers the single-error aggregation path.
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — removes the path: string[] parameter threaded through internal operation builders and centralizes procedure-path prefixing in the generator's error-collection catch block.
- Remove
pathfrombuildRequest/buildSuccessResponse/etc. — internal functions inopenapi-generator-operation.tsno longer carry apathparameter for error messages. The procedure path is now prepended once inopenapi-generator.ts's catch block (Procedure at ${path.join('.') || '(root)'}:). - Error message format tweak — procedure identification changed from
Procedure "path":toProcedure at path:, and root-level procedures now display asProcedure at (root):instead of the ambiguousProcedure ".":left by the old join. - Test call sites updated — all unit test assertions for
buildRequestandbuildSuccessResponsedrop the trailing['test']path argument.
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — documents paramsStyles on generated OpenAPI path parameters and fixes a duplicate JSDoc tag.
- Path parameter style documentation —
renderPathParametersnow emits OpenAPIstyleandexplodeattributes when the meta declares comma-delimited styles (comma-delimited-arrayandcomma-delimited-objectboth map tostyle: 'simple'withexplode: false). - JSDoc fix — removed a spurious duplicate
@defaulttag fromOpenAPIHandlerCodecCoreOptions.errorStatusMap.
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — removes the additionalProperties: false stripping in normalizeHoistedDefSchema so hoisted components faithfully reflect their source schema semantics.
- Preserve
additionalProperties: false—normalizeHoistedDefSchemano longer stripsadditionalProperties: falsewhen hoisting defs into#/components/schemas. Previously strict schemas (e.g.z.object()output direction) lost their strictness as components. z.looseObjectfor recursive entities — two e2e tests that model recursive relationships switched fromz.object()toz.looseObject(), keeping test assertions stable now that strictness is preserved.- Direction-specific component test — new e2e test verifies that a strict
z.object()used on both input and output produces separatePlanet(noadditionalProperties: false, input direction) andPlanet2(withadditionalProperties: false, output direction) components.
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — restricts component reuse to the same name family so different names always produce separate components.
- Name-family reuse —
resolveComponentNamereplacesfindUniqueComponentNameandfindReusableComponentName, walking onlyname,name2,name3, ... Equal schemas under unrelated names are never merged, reflecting the intent that different names signal different purposes. - Sibling def collision handling —
claimedNamesset prevents sibling defs in the same hoist call from colliding on the same numbered slot before their schemas are written. - Test coverage — unit test renamed to "does not merge equal schemas registered under different names", plus two new tests for name-family reuse and sibling def collisions. E2E test expanded to two procedures sharing the same schema, confirming cross-procedure reuse works within the name family.
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — extracts an isBodylessMethod utility so HEAD shares GET's bodyless semantics across the handler, link codec, and generator.
isBodylessMethodutility — new function inutils.tsreturningtruefor GET and HEAD, replacing three separatemethodchecks in the handler codec, two in the link codec, and three in the generator operation module.- HEAD parity in all layers — handler decodes HEAD inputs from query/path params without reading the body, link codec encodes HEAD requests with query params and no body, and the generator maps HEAD compact inputs to query parameters (same as GET).
- Test coverage — unit tests for
isBodylessMethod, HEAD handling in handler/link codec, generator components (compact mapping + error on non-object input), and an e2e CRUD test with aplanet.existsHEAD procedure.
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — replaces opaque numeric component suffixes with direction-aware names (PlanetInput/PlanetOutput) so generated OpenAPI documents communicate at a glance whether a component was hoisted from the input or output direction.
componentNameCandidate— new function that generates names withInput/Outputsuffixes when the conversion direction is known, falling back to the existing numeric scheme when direction isundefined.- Threaded
directionthroughhoistDefsandtoOpenAPISchema— both methods now accept an optionaldirectionparameter. Every call site inopenapi-generator-operation.tspasses the corresponding'input'or'output'value. - Test coverage — three new unit tests (directed suffix on bare name conflict, directed name family reuse + numbering) and updated e2e assertions from
Planet2→PlanetOutput,Moon2→MoonInput.
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — refines componentNameCandidate so the numeric postfix tail is shared across all conversion directions, allowing cross-direction component reuse.
- Shared numeric tail —
componentNameCandidatenow uses the bare def name as the base for numbered postfixes (e.g.Planet2,Planet3) regardless of direction, instead of nesting them under the directed name (PlanetOutput2). This means an input-direction variant that lands inPlanet2is reusable by a later output-direction call with the same schema. - Updated name-family JSDoc — documents the three-tier family: bare name → direction-suffixed name → shared numeric tail.
- New test coverage — "reuses numeric-tail slots across directions" verifies cross-direction reuse; existing test renamed to match the new behavior.
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — extends the component name-resolution algorithm so schemas that equal an existing component registered under the opposite direction's name are reused instead of minted as duplicates.
componentNameCandidateextracted — moved from an inline expression inresolveComponentNameto a standalone function returning[componentName: string, mintable: boolean, tail: boolean]. The opposite-direction name (e.g.PlanetInputwhen resolving an output-direction def) enters the family at attempt 3 withmintable: false— checked for equivalence but never minted under.- Delayed minting in
resolveComponentName— instead of committing to the first free slot, the loop records the earliest free mintable name inmintNameand keeps probing through the opposite-direction slot. Minting commits only when the numeric tail is reached, ensuring every reuse opportunity in the name family is checked first. - Two new unit tests — "reuses an equal component from the opposite direction instead of minting a duplicate" and "prefers reusing a directed component over minting the free bare name".
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
⚠️ Human review recommended
It significantly refactors core OpenAPI generation and schema-hoisting logic (including recursion/equivalence handling), so it warrants final human review despite strong test coverage.
Pull request overview
This PR refactors the packages/openapi OpenAPI generator into smaller modules, adds a component registry to aggressively hoist/reuse schemas (including typed error bodies) under #/components/schemas, and extends JSON Schema ref traversal in @orpc/json-schema to prevent $defs ref rewriting from drifting or missing keywords.
Changes:
- Split generation responsibilities into dedicated modules (
openapi-generator-operation,openapi-generator-components) and rewireOpenAPIGeneratorto orchestrate via a shared operation context. - Hoist defined ORPC error response schemas into
components.schemaswith stable, code-derived component names (and deterministic conflict handling / reuse acrossbaseregeneration). - Add
visitJsonSchemaRefsto share traversal rules withmapJsonSchemaRefs, improving$refdiscovery/rewrite for keywords likepatternProperties,contains, etc., and expand E2E/unit test coverage around these behaviors.
File summaries
| File | Description |
|---|---|
| packages/openapi/tests/openapi-generator/typed-errors.test.ts | E2E coverage for typed error responses referencing hoisted error components. |
| packages/openapi/tests/openapi-generator/schema-libraries.test.ts | E2E coverage for zod/arktype/plain-JSON-schema parity and $defs hoisting. |
| packages/openapi/tests/openapi-generator/reusable-components.test.ts | E2E coverage for recursive schemas, direction-specific schemas, and base-component reuse. |
| packages/openapi/tests/openapi-generator/file-transfer.test.ts | E2E coverage for upload/download/file unions and content-type splitting/merging. |
| packages/openapi/tests/openapi-generator/event-streaming.test.ts | E2E coverage for SSE request/response schemas derived from AsyncIteratorObject. |
| packages/openapi/tests/openapi-generator/detailed-http.test.ts | E2E coverage for detailed input/output structure mapping (params/query/headers/body, multi-status). |
| packages/openapi/tests/openapi-generator/crud.test.ts | E2E coverage for typical CRUD routing, parameter styles, and aggregated validation errors. |
| packages/openapi/tests/openapi-generator/composed-schemas.test.ts | E2E coverage for discriminated unions, intersections, and multi-call input/output merging. |
| packages/openapi/tests/shared/schema.ts | Test-only schema converters (zod + plain JSON schema wrapper) to validate generator agnosticism. |
| packages/openapi/src/utils.ts | Add isBodylessMethod helper for shared GET/HEAD “no body” handling. |
| packages/openapi/src/utils.test.ts | Unit tests for isBodylessMethod. |
| packages/openapi/src/openapi-generator.ts | Rewire generator to use modular operation builders + component registry; improve aggregated error message format. |
| packages/openapi/src/openapi-generator-operation.ts | New operation builder module: request/response generation, typed error component registration, media-type logic. |
| packages/openapi/src/openapi-generator-operation.test.ts | Unit tests for operation builders (requests, detailed outputs, errors, SSE, file content behavior). |
| packages/openapi/src/openapi-generator-components.ts | New component registry: hoists $defs to components, rewrites refs, resolves reuse/name conflicts (incl. recursion). |
| packages/openapi/src/openapi-generator-components.test.ts | Unit tests for hoisting, name resolution, reuse rules, recursion, and shouldHoistDef behavior. |
| packages/openapi/src/adapters/standard/openapi-link-codec.ts | Treat HEAD like GET for query serialization/no-body behavior via isBodylessMethod. |
| packages/openapi/src/adapters/standard/openapi-link-codec.test.ts | Add test ensuring HEAD encodes query params and no request body. |
| packages/openapi/src/adapters/standard/openapi-handler-codec.ts | Treat HEAD like GET when decoding compact inputs (query-only, no body read). |
| packages/openapi/src/adapters/standard/openapi-handler-codec.test.ts | Add test ensuring HEAD decoding merges params+query without reading the body. |
| packages/json-schema/src/ref-utils.ts | Add visitJsonSchemaRefs to traverse $refs using the same keyword set as mapJsonSchemaRefs. |
| packages/json-schema/src/ref-utils.test.ts | Unit tests asserting visitJsonSchemaRefs matches mapJsonSchemaRefs traversal and handles cycles/shared objects. |
Review details
- Files reviewed: 22/23 changed files
- Comments generated: 0
- Review effort level: Low
Note
Your feedback helps us improve the quality of this feature.
Please use 👍 or 👎 to tell us whether this assessment is correct.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Resolves: #1355
Summary
Rewrites the OpenAPI generator into small focused modules, hoists typed error schemas into
components.schemas, and brings every generator module to 100% test coverage. The generator stays schema-library agnostic: tests cover zod, arktype, and plain JSON schemas through custom converters.Error responses now reference named components derived from the error code:
Changes
#/components/schemas, named after their code (BAD_REQUEST->BadRequest), with numbered postfixes on conflicts and reuse of equal schemas across procedures.$defsrefs when local defs are referenced through keywords likepatternPropertiesorcontains.base.openapi-generator,openapi-generator-operation, andopenapi-generator-components.visitJsonSchemaRefsto@orpc/json-schema, sharing traversal rules withmapJsonSchemaRefsso ref discovery and rewriting cannot drift.Tests
src/, reaching 100% statement, branch, function, and line coverage of all three generator modules from unit tests alone.packages/openapi/tests/openapi-generator/organized around end-user use cases: CRUD API, webhook with full HTTP control, file upload and download, SSE streaming, typed errors, reusable components, composed schemas, and schema library agnosticism (zod, arktype, plain JSON schemas).