Skip to content

feat(openapi): modularize generator and hoist error schemas into components#1721

Merged
dinwwwh merged 18 commits into
middleapi:mainfrom
dinwwwh:claude/openapi-generator-test-refactor-f44ee2
Jul 25, 2026
Merged

feat(openapi): modularize generator and hoist error schemas into components#1721
dinwwwh merged 18 commits into
middleapi:mainfrom
dinwwwh:claude/openapi-generator-test-refactor-f44ee2

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Jul 24, 2026

Copy link
Copy Markdown
Member

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:

"400":
  content:
    application/json:
      schema:
        oneOf:
          - $ref: '#/components/schemas/BadRequest'
          - $ref: '#/components/schemas/BadRequest2'
          - $ref: '#/components/schemas/UndefinedError'

Changes

  • Defined errors are hoisted into #/components/schemas, named after their code (BAD_REQUEST -> BadRequest), with numbered postfixes on conflicts and reuse of equal schemas across procedures.
  • Fixed dangling $defs refs when local defs are referenced through keywords like patternProperties or contains.
  • Fixed component reuse when regenerating with a previously generated document passed as base.
  • Split the generator into openapi-generator, openapi-generator-operation, and openapi-generator-components.
  • Added visitJsonSchemaRefs to @orpc/json-schema, sharing traversal rules with mapJsonSchemaRefs so ref discovery and rewriting cannot drift.

Tests

  • One unit test file per module in src/, reaching 100% statement, branch, function, and line coverage of all three generator modules from unit tests alone.
  • E2E tests in 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).

@vercel

vercel Bot commented Jul 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
orpc Ready Ready Preview, Comment Jul 25, 2026 12:42am

@pkg-pr-new

pkg-pr-new Bot commented Jul 24, 2026

Copy link
Copy Markdown
More templates

@orpc/ai-sdk

npm i https://pkg.pr.new/@orpc/ai-sdk@1721

@orpc/arktype

npm i https://pkg.pr.new/@orpc/arktype@1721

@orpc/bun

npm i https://pkg.pr.new/@orpc/bun@1721

@orpc/client

npm i https://pkg.pr.new/@orpc/client@1721

@orpc/cloudflare

npm i https://pkg.pr.new/@orpc/cloudflare@1721

@orpc/contract

npm i https://pkg.pr.new/@orpc/contract@1721

@orpc/experimental-effect

npm i https://pkg.pr.new/@orpc/experimental-effect@1721

@orpc/evlog

npm i https://pkg.pr.new/@orpc/evlog@1721

@orpc/json-schema

npm i https://pkg.pr.new/@orpc/json-schema@1721

@orpc/nest

npm i https://pkg.pr.new/@orpc/nest@1721

@orpc/next

npm i https://pkg.pr.new/@orpc/next@1721

@orpc/openapi

npm i https://pkg.pr.new/@orpc/openapi@1721

@orpc/opentelemetry

npm i https://pkg.pr.new/@orpc/opentelemetry@1721

@orpc/pinia-colada

npm i https://pkg.pr.new/@orpc/pinia-colada@1721

@orpc/pino

npm i https://pkg.pr.new/@orpc/pino@1721

@orpc/publisher

npm i https://pkg.pr.new/@orpc/publisher@1721

@orpc/ratelimit

npm i https://pkg.pr.new/@orpc/ratelimit@1721

@orpc/server

npm i https://pkg.pr.new/@orpc/server@1721

@orpc/shared

npm i https://pkg.pr.new/@orpc/shared@1721

@orpc/tanstack-query

npm i https://pkg.pr.new/@orpc/tanstack-query@1721

@orpc/trpc

npm i https://pkg.pr.new/@orpc/trpc@1721

@orpc/valibot

npm i https://pkg.pr.new/@orpc/valibot@1721

@orpc/zod

npm i https://pkg.pr.new/@orpc/zod@1721

commit: b0e92cc

@codecov

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed-hq

codspeed-hq Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 18 untouched benchmarks


Comparing dinwwwh:claude/openapi-generator-test-refactor-f44ee2 (b0e92cc) with main (fb2b8d2)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (427926b) during the generation of this report, so fb2b8d2 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 modularizationopenapi-generator.ts shrinks to ~190 lines of orchestration, delegating schema hoisting to OpenAPIComponentRegistry and request/response/error building to buildRequest/buildSuccessResponse/buildErrorResponse.
  • Error schema hoisting — defined error responses now reference named $ref components derived from error codes (e.g. NOT_FOUNDNotFound), with numbered postfixes on name conflicts and reuse of equal schemas across procedures.
  • visitJsonSchemaRefs — shared traversal in @orpc/json-schema that walks the same keywords as mapJsonSchemaRefs, preventing ref-discovery drift.
  • Test restructuring — single 3900-line test file replaced by 14 focused test files plus shared testSchema/testSchemaConverter helpers for schema-library-agnostic coverage.
  • Component reuse fixes — fixed dangling $defs refs in patternProperties/contains and fixed reuse against a previously generated base document.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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.ts slimmed to ~189 lines — private request/successResponse/errorResponse methods replaced by calls to buildRequest/buildSuccessResponse/buildErrorResponse; OpenAPIGeneratorError and the OpenAPIOperationContext contract are now defined in openapi-generator-operation.ts and re-exported.
  • visitJsonSchemaRefs — new traversal utility in @orpc/json-schema that mirrors mapJsonSchemaRefs's keyword set, with shared/cyclic instance dedup. Tested against the same keywords mapJsonSchemaRefs rewrites.
  • Test reorganization — separate *-schema, *-error-response, *-hoisting, and *-request-body test files merged into openapi-generator.test.ts (18 orchestration tests), while per-module unit tests stay in openapi-generator-operation.test.ts (35) and openapi-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. Shared testSchema/testSchemaConverter helpers live in tests/__shared__/schema.ts.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@dinwwwh dinwwwh changed the title refactor(openapi)!: modularize generator and hoist error schemas into components refactor(openapi): modularize generator and hoist error schemas into components Jul 24, 2026

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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, and buildErrorResponse now identifies the procedure with a cleaner format (Procedure "path": instead of Procedure 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.message directly 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.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 path from buildRequest/buildSuccessResponse/etc. — internal functions in openapi-generator-operation.ts no longer carry a path parameter for error messages. The procedure path is now prepended once in openapi-generator.ts's catch block (Procedure at ${path.join('.') || '(root)'}:).
  • Error message format tweak — procedure identification changed from Procedure "path": to Procedure at path:, and root-level procedures now display as Procedure at (root): instead of the ambiguous Procedure ".": left by the old join.
  • Test call sites updated — all unit test assertions for buildRequest and buildSuccessResponse drop the trailing ['test'] path argument.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes — documents paramsStyles on generated OpenAPI path parameters and fixes a duplicate JSDoc tag.

  • Path parameter style documentationrenderPathParameters now emits OpenAPI style and explode attributes when the meta declares comma-delimited styles (comma-delimited-array and comma-delimited-object both map to style: 'simple' with explode: false).
  • JSDoc fix — removed a spurious duplicate @default tag from OpenAPIHandlerCodecCoreOptions.errorStatusMap.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes — removes the additionalProperties: false stripping in normalizeHoistedDefSchema so hoisted components faithfully reflect their source schema semantics.

  • Preserve additionalProperties: falsenormalizeHoistedDefSchema no longer strips additionalProperties: false when hoisting defs into #/components/schemas. Previously strict schemas (e.g. z.object() output direction) lost their strictness as components.
  • z.looseObject for recursive entities — two e2e tests that model recursive relationships switched from z.object() to z.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 separate Planet (no additionalProperties: false, input direction) and Planet2 (with additionalProperties: false, output direction) components.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes — restricts component reuse to the same name family so different names always produce separate components.

  • Name-family reuseresolveComponentName replaces findUniqueComponentName and findReusableComponentName, walking only name, name2, name3, ... Equal schemas under unrelated names are never merged, reflecting the intent that different names signal different purposes.
  • Sibling def collision handlingclaimedNames set 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.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes — extracts an isBodylessMethod utility so HEAD shares GET's bodyless semantics across the handler, link codec, and generator.

  • isBodylessMethod utility — new function in utils.ts returning true for GET and HEAD, replacing three separate method checks 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 a planet.exists HEAD procedure.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 with Input/Output suffixes when the conversion direction is known, falling back to the existing numeric scheme when direction is undefined.
  • Threaded direction through hoistDefs and toOpenAPISchema — both methods now accept an optional direction parameter. Every call site in openapi-generator-operation.ts passes 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 Planet2PlanetOutput, Moon2MoonInput.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 tailcomponentNameCandidate now 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 in Planet2 is 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.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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.

  • componentNameCandidate extracted — moved from an inline expression in resolveComponentName to a standalone function returning [componentName: string, mintable: boolean, tail: boolean]. The opposite-direction name (e.g. PlanetInput when resolving an output-direction def) enters the family at attempt 3 with mintable: 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 in mintName and 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".

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@dinwwwh dinwwwh changed the title refactor(openapi): modularize generator and hoist error schemas into components feat(openapi): modularize generator and hoist error schemas into components Jul 24, 2026
@dinwwwh
dinwwwh requested a review from Copilot July 24, 2026 23:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 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 rewire OpenAPIGenerator to orchestrate via a shared operation context.
  • Hoist defined ORPC error response schemas into components.schemas with stable, code-derived component names (and deterministic conflict handling / reuse across base regeneration).
  • Add visitJsonSchemaRefs to share traversal rules with mapJsonSchemaRefs, improving $ref discovery/rewrite for keywords like patternProperties, 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.

@dinwwwh
dinwwwh merged commit deb9e75 into middleapi:main Jul 25, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add orpc error schemas to common schemas in OpenAPI

2 participants