├── .gitattributes ├── src ├── index.ts ├── meta.ts ├── dummyRouter.ts ├── generate.test.ts ├── generate.ts └── __snapshots__ │ └── generate.test.ts.snap ├── config ├── jest.config.json ├── rig.json └── api-extractor.json ├── tsconfig.json ├── scripts ├── release ├── generate-api-docs └── serve-example ├── .prettierrc.yml ├── etc ├── README.md └── openapi-trpc.api.md ├── .prettierignore ├── .changeset ├── config.json └── README.md ├── .gitignore ├── tests └── example.spec.ts ├── tsconfig-base.json ├── CHANGELOG.md ├── .github ├── actions │ └── setup │ │ └── action.yml └── workflows │ ├── publish.yml │ └── ci.yml ├── package.json ├── playwright.config.ts └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | *.md text 2 | *.png binary 3 | *.jpg binary 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export type { OperationMeta } from './meta' 2 | export * from './generate' 3 | -------------------------------------------------------------------------------- /config/jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@rushstack/heft-web-rig/profiles/library/config/jest.config.json", 3 | "collectCoverage": true 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig-base.json", 3 | "compilerOptions": { 4 | "types": ["heft-jest", "node"], 5 | "target": "ES2020", 6 | "lib": ["ES2020"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /scripts/release: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # This file is automatically managed by . 4 | # Any manual changes to this file may be overwritten. 5 | 6 | changeset publish 7 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically managed by . 2 | # Any manual changes to this file may be overwritten. 3 | 4 | singleQuote: true 5 | semi: false 6 | trailingComma: all 7 | -------------------------------------------------------------------------------- /etc/README.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | This directory contains a report file generated by [API Extractor](https://api-extractor.com/). 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # This file is automatically managed by . 2 | # Any manual changes to this file may be overwritten. 3 | 4 | /dist/ 5 | /etc/ 6 | /lib/ 7 | /lib-commonjs/ 8 | pnpm-lock.yaml 9 | CHANGELOG.md 10 | /temp/ 11 | /.changeset/ -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.6.4/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "linked": [], 6 | "access": "public", 7 | "baseBranch": "main", 8 | "updateInternalDependencies": "patch", 9 | "ignore": [] 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This file is automatically managed by . 2 | # Any manual changes to this file may be overwritten. 3 | 4 | /lib/ 5 | /lib-commonjs/ 6 | /dist/ 7 | node_modules 8 | .heft 9 | temp 10 | tmp 11 | .DS_Store 12 | .data 13 | /test-results/ 14 | /playwright-report/ 15 | /playwright/.cache/ 16 | -------------------------------------------------------------------------------- /config/rig.json: -------------------------------------------------------------------------------- 1 | // This file is automatically managed by . 2 | // Any manual changes to this file may be overwritten. 3 | 4 | { 5 | "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", 6 | 7 | "rigPackageName": "@rushstack/heft-web-rig", 8 | "rigProfile": "library" 9 | } 10 | -------------------------------------------------------------------------------- /tests/example.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test' 2 | import fs from 'fs' 3 | 4 | test('basic', async ({ page }) => { 5 | await page.goto('/') 6 | await expect(page.getByText('tRPC HTTP-RPC')).toBeVisible() 7 | fs.mkdirSync('temp/examples', { recursive: true }) 8 | await page.screenshot({ path: 'temp/examples/basic.png' }) 9 | }) 10 | -------------------------------------------------------------------------------- /tsconfig-base.json: -------------------------------------------------------------------------------- 1 | // This file is automatically managed by . 2 | // Any manual changes to this file may be overwritten. 3 | 4 | { 5 | "extends": "./node_modules/@rushstack/heft-web-rig/profiles/library/tsconfig-base.json", 6 | "compilerOptions": { 7 | "skipLibCheck": true, 8 | "importHelpers": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # openapi-trpc 2 | 3 | ## 0.2.1 4 | 5 | ### Patch Changes 6 | 7 | - 52e8fa7: Support optional Zod input schema in router definitions 8 | 9 | ## 0.2.0 10 | 11 | ### Minor Changes 12 | 13 | - 32f04c4: Allow specifying ZodArray as in input 14 | 15 | ## 0.1.2 16 | 17 | ### Patch Changes 18 | 19 | - 503f8b8: Fixed a bug where the output schema is incorrect 20 | -------------------------------------------------------------------------------- /scripts/generate-api-docs: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # This file is automatically managed by . 4 | # Any manual changes to this file may be overwritten. 5 | 6 | for I in temp/api/*.json; do 7 | # A weird issue with API Extractor adds `_2` suffix to some function names. 8 | # https://github.com/microsoft/rushstack/issues/2895 9 | sed -i.bak 's/_2//g' "$I" 10 | done 11 | 12 | cp temp/api/*.json dist/ 13 | -------------------------------------------------------------------------------- /src/meta.ts: -------------------------------------------------------------------------------- 1 | import { OpenAPIV3 } from 'openapi-types' 2 | 3 | function defineKeys(keys: T[]): T[] { 4 | return keys 5 | } 6 | 7 | export const allowedOperationKeys = defineKeys([ 8 | 'deprecated', 9 | 'description', 10 | 'externalDocs', 11 | 'summary', 12 | 'tags', 13 | ]) 14 | 15 | /** 16 | * @public 17 | */ 18 | export interface OperationMeta 19 | extends Pick< 20 | OpenAPIV3.OperationObject, 21 | typeof allowedOperationKeys[number] 22 | > {} 23 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically managed by . 2 | # Any manual changes to this file may be overwritten. 3 | 4 | name: 'Sets up project' 5 | description: 'Installs Node.js, pnpm, and project dependencies.' 6 | runs: 7 | using: 'composite' 8 | steps: 9 | - name: Setup pnpm 10 | uses: pnpm/action-setup@v4 11 | - name: Setup Node.js 12 | uses: actions/setup-node@v4 13 | with: 14 | node-version: 22 15 | cache: pnpm 16 | - name: Install dependencies and build 17 | run: pnpm install --prefer-offline 18 | shell: bash 19 | -------------------------------------------------------------------------------- /src/dummyRouter.ts: -------------------------------------------------------------------------------- 1 | import { initTRPC } from '@trpc/server' 2 | import { z } from 'zod' 3 | import { OperationMeta } from './meta' 4 | 5 | export const createDummyRouter = ( 6 | t = initTRPC.meta().create(), 7 | ) => { 8 | return t.router({ 9 | hello: t.router({ 10 | world: t.procedure 11 | .meta({ description: 'ok' }) 12 | .input(z.object({ name: z.string() })) 13 | .output(z.string()) 14 | .query(() => 'hello world'), 15 | }), 16 | }) 17 | } 18 | 19 | export type DummyRouter = ReturnType 20 | export type DummyProcedure = ReturnType< 21 | typeof createDummyRouter 22 | >['_def']['procedures']['hello']['_def']['procedures']['world']['_def'] 23 | -------------------------------------------------------------------------------- /scripts/serve-example: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fastify = require('fastify')({ logger: true }) 4 | const fastifyStatic = require('@fastify/static') 5 | 6 | fastify.get('/swagger-initializer.js', async (request, reply) => { 7 | reply.header('Content-Type', 'application/javascript') 8 | return `window.onload = ${function () { 9 | window.ui = SwaggerUIBundle({ 10 | url: '/examples/basic.json', 11 | dom_id: '#swagger-ui', 12 | deepLinking: true, 13 | presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset], 14 | plugins: [SwaggerUIBundle.plugins.DownloadUrl], 15 | layout: 'StandaloneLayout', 16 | }) 17 | }};` 18 | }) 19 | 20 | fastify.register(fastifyStatic, { 21 | root: require('swagger-ui-dist').getAbsoluteFSPath(), 22 | }) 23 | 24 | fastify.register(fastifyStatic, { 25 | root: require('path').join(__dirname, '../temp/examples'), 26 | prefix: '/examples', 27 | decorateReply: false, 28 | }) 29 | 30 | fastify.listen({ port: 51201 }) 31 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically managed by . 2 | # Any manual changes to this file may be overwritten. 3 | 4 | name: Publish 5 | on: 6 | push: 7 | branches: 8 | - main 9 | concurrency: ${{ github.workflow }}-${{ github.ref }} 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-22.04 14 | permissions: 15 | contents: write 16 | pull-requests: write 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | - name: Setup 23 | uses: ./.github/actions/setup 24 | - name: Create Release Pull Request or Publish to npm 25 | id: changesets 26 | uses: changesets/action@2a025e8ab1cfa4312c2868cb6aa3cd3b473b84bf # v1.3.0 27 | with: 28 | publish: pnpm run release 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 32 | -------------------------------------------------------------------------------- /etc/openapi-trpc.api.md: -------------------------------------------------------------------------------- 1 | ## API Report File for "openapi-trpc" 2 | 3 | > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). 4 | 5 | ```ts 6 | 7 | import { OpenAPIV3 } from 'openapi-types'; 8 | import { RootConfig } from '@trpc/server'; 9 | import { Router } from '@trpc/server'; 10 | import { RouterDef } from '@trpc/server'; 11 | 12 | // Warning: (ae-forgotten-export) The symbol "MetaOf" needs to be exported by the entry point index.d.ts 13 | // 14 | // @public (undocumented) 15 | export function generateOpenAPIDocumentFromTRPCRouter>(inRouter: R, options?: GenerateOpenAPIDocumentOptions>): OpenAPIV3.Document<{}>; 16 | 17 | // @public (undocumented) 18 | export interface GenerateOpenAPIDocumentOptions { 19 | // (undocumented) 20 | pathPrefix?: string; 21 | // (undocumented) 22 | processOperation?: (operation: OpenAPIV3.OperationObject, meta: M | undefined) => OpenAPIV3.OperationObject | void; 23 | } 24 | 25 | // Warning: (ae-forgotten-export) The symbol "allowedOperationKeys" needs to be exported by the entry point index.d.ts 26 | // 27 | // @public (undocumented) 28 | export interface OperationMeta extends Pick { 29 | } 30 | 31 | // (No @packageDocumentation comment for this package) 32 | 33 | ``` 34 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically managed by . 2 | # Any manual changes to this file may be overwritten. 3 | 4 | name: CI 5 | on: 6 | push: 7 | branches: 8 | - main 9 | pull_request: 10 | branches: 11 | - main 12 | jobs: 13 | test: 14 | name: Test 15 | runs-on: ubuntu-22.04 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | - name: Setup 22 | uses: ./.github/actions/setup 23 | - name: Test 24 | run: pnpm run test 25 | - name: SonarCloud Scan 26 | uses: SonarSource/sonarcloud-github-action@master 27 | if: env.SONAR_TOKEN 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any 30 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 31 | lint: 32 | name: Lint 33 | runs-on: ubuntu-22.04 34 | steps: 35 | - name: Checkout 36 | uses: actions/checkout@v4 37 | with: 38 | fetch-depth: 0 39 | - name: Setup 40 | uses: ./.github/actions/setup 41 | - name: Lint 42 | run: pnpm run format 43 | - name: Lint 44 | run: pnpm run api 45 | - name: Stage changed files 46 | run: git add --update 47 | - name: Ensure no file is changed 48 | uses: dtinth/patch-generator-action@v1 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openapi-trpc", 3 | "version": "0.2.1", 4 | "files": [ 5 | "src", 6 | "lib", 7 | "lib-commonjs", 8 | "dist" 9 | ], 10 | "main": "./lib-commonjs/index.js", 11 | "module": "./lib/index.js", 12 | "types": "./dist/openapi-trpc.d.ts", 13 | "docModel": "./dist/openapi-trpc.api.json", 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/dtinth/openapi-trpc.git" 17 | }, 18 | "homepage": "https://github.com/dtinth/openapi-trpc#readme", 19 | "bugs": { 20 | "url": "https://github.com/dtinth/openapi-trpc/issues" 21 | }, 22 | "devDependencies": { 23 | "@changesets/cli": "2.25.0", 24 | "@fastify/static": "^6.9.0", 25 | "@playwright/test": "^1.30.0", 26 | "@rushstack/heft": "0.48.7", 27 | "@rushstack/heft-web-rig": "0.12.10", 28 | "@trpc/server": "^10.11.1", 29 | "@types/heft-jest": "1.0.3", 30 | "@types/node": "^18.13.0", 31 | "fastify": "^4.13.0", 32 | "npm-run-all": "^4.1.5", 33 | "prettier": "2.7.1", 34 | "swagger-ui-dist": "^4.15.5", 35 | "zod": "^3.20.6" 36 | }, 37 | "peerDependencies": { 38 | "@trpc/server": "^10.11.1", 39 | "zod": "^3.20.6" 40 | }, 41 | "scripts": { 42 | "build": "heft build", 43 | "test": "heft test", 44 | "prepare": "heft build && ./scripts/generate-api-docs", 45 | "release": "./scripts/release", 46 | "format": "prettier --write .", 47 | "api": "./scripts/generate-api-docs" 48 | }, 49 | "dependencies": { 50 | "openapi-types": "^12.1.0", 51 | "zod-to-json-schema": "^3.20.3" 52 | }, 53 | "packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977" 54 | } 55 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test' 2 | 3 | /** 4 | * Read environment variables from file. 5 | * https://github.com/motdotla/dotenv 6 | */ 7 | // require('dotenv').config(); 8 | 9 | /** 10 | * See https://playwright.dev/docs/test-configuration. 11 | */ 12 | export default defineConfig({ 13 | testDir: './tests', 14 | /* Maximum time one test can run for. */ 15 | timeout: 30 * 1000, 16 | expect: { 17 | /** 18 | * Maximum time expect() should wait for the condition to be met. 19 | * For example in `await expect(locator).toHaveText();` 20 | */ 21 | timeout: 5000, 22 | }, 23 | /* Run tests in files in parallel */ 24 | fullyParallel: true, 25 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 26 | forbidOnly: !!process.env.CI, 27 | /* Retry on CI only */ 28 | retries: process.env.CI ? 2 : 0, 29 | /* Opt out of parallel tests on CI. */ 30 | workers: process.env.CI ? 1 : undefined, 31 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 32 | reporter: 'html', 33 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 34 | use: { 35 | /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ 36 | actionTimeout: 0, 37 | /* Base URL to use in actions like `await page.goto('/')`. */ 38 | baseURL: 'http://localhost:51201', 39 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 40 | trace: 'on-first-retry', 41 | screenshot: 'on', 42 | }, 43 | 44 | /* Configure projects for major browsers */ 45 | projects: [ 46 | { 47 | name: 'chromium', 48 | use: { ...devices['Desktop Chrome'] }, 49 | }, 50 | ], 51 | 52 | /* Folder for test artifacts such as screenshots, videos, traces, etc. */ 53 | // outputDir: 'test-results/', 54 | 55 | /* Run your local dev server before starting the tests */ 56 | webServer: { 57 | command: './scripts/serve-example', 58 | port: 51201, 59 | }, 60 | }) 61 | -------------------------------------------------------------------------------- /src/generate.test.ts: -------------------------------------------------------------------------------- 1 | import { initTRPC } from '@trpc/server' 2 | import fs from 'fs' 3 | import { z } from 'zod' 4 | import { createDummyRouter } from './dummyRouter' 5 | import { generateOpenAPIDocumentFromTRPCRouter } from './generate' 6 | import { OperationMeta } from './meta' 7 | 8 | it('works', () => { 9 | const t = initTRPC.meta().create() 10 | const router = t.router({ 11 | example: t.router({ 12 | createGreeting: t.procedure 13 | .meta({ summary: 'Creates a greeting' }) 14 | .input( 15 | z 16 | .object({ 17 | name: z.string().optional().describe('The name to greet'), 18 | }) 19 | .describe('Input for createGreeting'), 20 | ) 21 | .output(z.string().describe('The greeting text')) 22 | .mutation(() => 'ok'), 23 | hello: t.procedure 24 | .input(z.object({ text: z.string() })) 25 | .query(() => null), 26 | getAll: t.procedure 27 | .output(z.array(z.object({ id: z.string() }))) 28 | .query(() => []), 29 | }), 30 | dummy: createDummyRouter(t), 31 | }) 32 | const doc = generateOpenAPIDocumentFromTRPCRouter(router, { 33 | pathPrefix: '/trpc', 34 | }) 35 | fs.mkdirSync('temp/examples', { recursive: true }) 36 | fs.writeFileSync('temp/examples/basic.json', JSON.stringify(doc, null, 2)) 37 | expect(doc).toMatchSnapshot() 38 | }) 39 | 40 | it('works with array', () => { 41 | const t = initTRPC.meta().create() 42 | const router = t.router({ 43 | exampleWithArrayAsInput: t.router({ 44 | queryWithArrayInput: t.procedure 45 | .input(z.array(z.string())) 46 | .query(() => null), 47 | }), 48 | dummy: createDummyRouter(t), 49 | }) 50 | const doc = generateOpenAPIDocumentFromTRPCRouter(router, { 51 | pathPrefix: '/trpc', 52 | }) 53 | fs.mkdirSync('temp/examples', { recursive: true }) 54 | fs.writeFileSync('temp/examples/array.json', JSON.stringify(doc, null, 2)) 55 | expect(doc).toMatchSnapshot() 56 | }) 57 | 58 | it('lets you post-process each operation, giving typed access to the meta', () => { 59 | interface AppMeta extends OperationMeta { 60 | requiresAuth?: boolean 61 | } 62 | const t = initTRPC.meta().create() 63 | const router = t.router({ 64 | public: t.procedure.query(() => null), 65 | private: t.procedure.meta({ requiresAuth: true }).query(() => null), 66 | }) 67 | const doc = generateOpenAPIDocumentFromTRPCRouter(router, { 68 | pathPrefix: '/trpc', 69 | processOperation: (op, meta) => { 70 | if (meta?.requiresAuth) { 71 | op.security = [{ bearerAuth: [] }] 72 | } 73 | }, 74 | }) 75 | fs.mkdirSync('temp/examples', { recursive: true }) 76 | fs.writeFileSync( 77 | 'temp/examples/operation-processing.json', 78 | JSON.stringify(doc, null, 2), 79 | ) 80 | expect(doc).toMatchSnapshot() 81 | }) 82 | 83 | it('works with optional zod object', () => { 84 | const t = initTRPC.meta().create() 85 | const router = t.router({ 86 | example: t.router({ 87 | optionalObjectTest: t.procedure 88 | .input( 89 | z 90 | .object({ 91 | anOption: z.string(), 92 | }) 93 | .optional(), 94 | ) 95 | .query(() => null), 96 | }), 97 | dummy: createDummyRouter(t), 98 | }) 99 | const doc = generateOpenAPIDocumentFromTRPCRouter(router, { 100 | pathPrefix: '/trpc', 101 | }) 102 | fs.mkdirSync('temp/examples', { recursive: true }) 103 | fs.writeFileSync( 104 | 'temp/examples/optional-object.json', 105 | JSON.stringify(doc, null, 2), 106 | ) 107 | expect(doc).toMatchSnapshot() 108 | }) 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # openapi-trpc 2 | 3 | _Not to be confused with [trpc-openapi](https://github.com/jlalmes/trpc-openapi)._ 4 | 5 | The `openapi-trpc` package is a tool to generate OpenAPI v3 spec from a [tRPC](https://trpc.io) router, adhering to tRPC’s [HTTP RPC Specification](https://trpc.io/docs/rpc). This lets you take an existing tRPC router, and generate an OpenAPI spec from it. From there, you can use the spec to generate a client, or use it to document your API. 6 | 7 | ## Usage 8 | 9 | When initializing tRPC with `initTRPC`, add `.meta()` to the chain: 10 | 11 | ```ts 12 | import { initTRPC } from '@trpc/server' 13 | import { OperationMeta } from 'openapi-trpc' 14 | 15 | const t = initTRPC.meta().create() 16 | ``` 17 | 18 | Whenever you want to generate an OpenAPI spec, call `generateOpenAPIDocumentFromTRPCRouter()`: 19 | 20 | ```ts 21 | import { generateOpenAPIDocumentFromTRPCRouter } from 'openapi-trpc' 22 | import { appRouter } from './router' 23 | 24 | const doc = generateOpenAPIDocumentFromTRPCRouter(appRouter, { 25 | pathPrefix: '/trpc', 26 | }) 27 | ``` 28 | 29 | Inside your procedures, you can add metadata to the OpenAPI spec by adding `.meta()` to the chain. If the meta object contains the [`deprecated`, `description`, `externalDocs`, `summary`, or `tags` keys](https://swagger.io/specification/#operation-object), they will be added to the OpenAPI spec. 30 | 31 | In your Zod schema (yes, you must use Zod, as other schema libraries are not supported), you can use `.describe()` to add a description to each field, and they will be added to the schema in the OpenAPI spec (thanks to [zod-to-json-schema](https://www.npmjs.com/package/zod-to-json-schema)). 32 | 33 | ```ts 34 | t.procedure 35 | .meta({ summary: '…', description: '…' }) 36 | .input( 37 | z.object({ 38 | id: z.number().describe('…'), 39 | /* ... */ 40 | }), 41 | ) 42 | .query(() => { 43 | /* … */ 44 | }) 45 | ``` 46 | 47 | You can then use the `doc` to generate API documentation or a client. 48 | 49 | ![basic](https://user-images.githubusercontent.com/193136/218788215-f7f9892b-c120-403e-ba4d-ebf334f5a2a6.png) 50 | 51 | More advanced usage: 52 | 53 | - You can use your own type for the meta object, to [add extra metadata to the tRPC procedure](https://trpc.io/docs/metadata). It is recommended that the type should extend `OperationMeta`. 54 | 55 | - You can also provide `processOperation` to customize the OpenAPI spec on a per-operation (i.e. tRPC procedure) basis. This allows adding more metadata to the spec, such as `security` (for authentication) or `servers`. The function is called with the operation’s OpenAPI spec, and the tRPC procedure’s metadata. It may mutate the spec, or return a new one. For more information, [see the tests](./src/generate.test.ts). 56 | 57 | ## `openapi-trpc` vs `trpc-openapi` 58 | 59 | `openapi-trpc` (this library): 60 | 61 | - Generates an OpenAPI v3 document according to the existing [HTTP RPC Specification](https://trpc.io/docs/rpc). It is not RESTful, but matches how a normal tRPC client talks to the server. You API looks like this: `GET /trpc/sayHello?input={"name":"James"}`. 62 | - Does not require adding any extra request handlers to your app. 63 | - Unproven code, full of hacks, PoC-quality code. However the scope of the library is very small and it works well enough for me. 64 | 65 | [`trpc-openapi`](https://github.com/jlalmes/trpc-openapi): 66 | 67 | - Generates RESTful endpoints. You can customize the path and HTTP method of each endpoint. You get nice-looking RESTful APIs like `GET /say-hello?name=James`. This requires adding an extra request handler to your app [which comes with its own limitations](https://github.com/jlalmes/trpc-openapi#requirements). 68 | - Works with Express, Next.js, Serverless, and Node:HTTP servers. No adapters for Fetch, Fastify, Nuxt, or Workers yet. 69 | - Well-tested, well-documented and well-maintained. 70 | -------------------------------------------------------------------------------- /src/generate.ts: -------------------------------------------------------------------------------- 1 | import { zodToJsonSchema } from 'zod-to-json-schema' 2 | import { DummyProcedure, DummyRouter } from './dummyRouter' 3 | import { 4 | z, 5 | AnyZodObject, 6 | ZodType, 7 | ZodFirstPartyTypeKind, 8 | ZodArray, 9 | ZodTypeAny, 10 | } from 'zod' 11 | import { OpenAPIV3 } from 'openapi-types' 12 | import { OperationMeta, allowedOperationKeys } from './meta' 13 | import { RootConfig, Router, RouterDef } from '@trpc/server' 14 | 15 | /** 16 | * @public 17 | */ 18 | export function generateOpenAPIDocumentFromTRPCRouter>( 19 | inRouter: R, 20 | options: GenerateOpenAPIDocumentOptions> = {}, 21 | ) { 22 | const router: DummyRouter = inRouter as unknown as DummyRouter 23 | const procs = router._def.procedures 24 | const paths: OpenAPIV3.PathsObject = {} 25 | const processOperation = ( 26 | op: OpenAPIV3.OperationObject, 27 | meta: MetaOf, 28 | ): OpenAPIV3.OperationObject => { 29 | return options.processOperation?.(op, meta) || op 30 | } 31 | for (const [procName, proc] of Object.entries(procs)) { 32 | const procDef = proc._def as unknown as DummyProcedure 33 | 34 | // ZodArrays are also correct, as .splice(1) will return an empty array 35 | // it's ok just to return the array itself 36 | const input = 37 | getZodTypeName(procDef.inputs[0]) === ZodFirstPartyTypeKind.ZodArray 38 | ? (procDef.inputs[0] as ZodArray) 39 | : procDef.inputs 40 | .slice(1) 41 | .reduce( 42 | (acc, cur) => asZodObject(acc).merge(asZodObject(cur)), 43 | asZodObject(procDef.inputs[0] || z.object({})), 44 | ) 45 | const output = procDef.output 46 | const inputSchema = toJsonSchema(input) 47 | const outputSchema = output 48 | ? toJsonSchema( 49 | z.object({ 50 | result: z.object({ 51 | data: asZodType(output), 52 | }), 53 | }), 54 | ) 55 | : undefined 56 | const key = [ 57 | '', 58 | ...(options.pathPrefix || '/').split('/').filter(Boolean), 59 | procName, 60 | ].join('/') 61 | const responses = { 62 | 200: { 63 | description: (output && asZodType(output).description) || '', 64 | ...(outputSchema 65 | ? { 66 | content: { 67 | 'application/json': { 68 | schema: outputSchema as any, 69 | }, 70 | }, 71 | } 72 | : {}), 73 | }, 74 | } 75 | const operationInfo: Partial = { 76 | tags: procName.split('.').slice(0, -1).slice(0, 1), 77 | } 78 | for (const key of allowedOperationKeys) { 79 | const value = procDef.meta?.[key] 80 | if (value) { 81 | operationInfo[key] = value as any 82 | } 83 | } 84 | if (procDef.query) { 85 | paths[key] = { 86 | get: processOperation( 87 | { 88 | ...operationInfo, 89 | operationId: procName, 90 | responses, 91 | parameters: [ 92 | { 93 | in: 'query', 94 | name: 'input', 95 | content: { 96 | 'application/json': { 97 | schema: inputSchema as any, 98 | }, 99 | }, 100 | }, 101 | ], 102 | }, 103 | procDef.meta as any, 104 | ), 105 | } 106 | } else { 107 | paths[key] = { 108 | post: processOperation( 109 | { 110 | ...operationInfo, 111 | operationId: procName, 112 | responses, 113 | requestBody: { 114 | content: { 115 | 'application/json': { 116 | schema: inputSchema as any, 117 | }, 118 | }, 119 | }, 120 | }, 121 | procDef.meta as any, 122 | ), 123 | } 124 | } 125 | } 126 | const api: OpenAPIV3.Document = { 127 | openapi: '3.0.0', 128 | info: { 129 | title: 'tRPC HTTP-RPC', 130 | version: '', 131 | }, 132 | paths, 133 | } 134 | return api 135 | } 136 | 137 | function getZodTypeName(input: unknown) { 138 | return (input as { _def?: { typeName?: string } } | undefined)?._def?.typeName 139 | } 140 | 141 | function asZodObject(input: unknown) { 142 | if ( 143 | getZodTypeName(input) !== ZodFirstPartyTypeKind.ZodObject && 144 | getZodTypeName(input) !== ZodFirstPartyTypeKind.ZodVoid && 145 | getZodTypeName(input) !== ZodFirstPartyTypeKind.ZodOptional 146 | ) { 147 | throw new Error('Expected a ZodObject, received: ' + String(input)) 148 | } 149 | return input as AnyZodObject 150 | } 151 | 152 | function asZodType(input: unknown) { 153 | if (!getZodTypeName(input)) { 154 | throw new Error('Expected a Zod schema, received: ' + String(input)) 155 | } 156 | return input as ZodType 157 | } 158 | 159 | /** 160 | * @public 161 | */ 162 | export interface GenerateOpenAPIDocumentOptions { 163 | pathPrefix?: string 164 | processOperation?: ( 165 | operation: OpenAPIV3.OperationObject, 166 | meta: M | undefined, 167 | ) => OpenAPIV3.OperationObject | void 168 | } 169 | 170 | function toJsonSchema(input: ZodType) { 171 | const { $schema, ...output } = zodToJsonSchema(input) 172 | return output 173 | } 174 | 175 | type MetaOf> = R extends Router> 176 | ? D extends RootConfig 177 | ? C['meta'] 178 | : never 179 | : never 180 | -------------------------------------------------------------------------------- /config/api-extractor.json: -------------------------------------------------------------------------------- 1 | // This file is automatically managed by . 2 | // Any manual changes to this file may be overwritten. 3 | 4 | /** 5 | * Config file for API Extractor. For more info, please visit: https://api-extractor.com 6 | */ 7 | { 8 | "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", 9 | 10 | /** 11 | * Optionally specifies another JSON config file that this file extends from. This provides a way for 12 | * standard settings to be shared across multiple projects. 13 | * 14 | * If the path starts with "./" or "../", the path is resolved relative to the folder of the file that contains 15 | * the "extends" field. Otherwise, the first path segment is interpreted as an NPM package name, and will be 16 | * resolved using NodeJS require(). 17 | * 18 | * SUPPORTED TOKENS: none 19 | * DEFAULT VALUE: "" 20 | */ 21 | // "extends": "./shared/api-extractor-base.json" 22 | // "extends": "my-package/include/api-extractor-base.json" 23 | 24 | /** 25 | * Determines the "" token that can be used with other config file settings. The project folder 26 | * typically contains the tsconfig.json and package.json config files, but the path is user-defined. 27 | * 28 | * The path is resolved relative to the folder of the config file that contains the setting. 29 | * 30 | * The default value for "projectFolder" is the token "", which means the folder is determined by traversing 31 | * parent folders, starting from the folder containing api-extractor.json, and stopping at the first folder 32 | * that contains a tsconfig.json file. If a tsconfig.json file cannot be found in this way, then an error 33 | * will be reported. 34 | * 35 | * SUPPORTED TOKENS: 36 | * DEFAULT VALUE: "" 37 | */ 38 | // "projectFolder": "..", 39 | 40 | /** 41 | * (REQUIRED) Specifies the .d.ts file to be used as the starting point for analysis. API Extractor 42 | * analyzes the symbols exported by this module. 43 | * 44 | * The file extension must be ".d.ts" and not ".ts". 45 | * 46 | * The path is resolved relative to the folder of the config file that contains the setting; to change this, 47 | * prepend a folder token such as "". 48 | * 49 | * SUPPORTED TOKENS: , , 50 | */ 51 | "mainEntryPointFilePath": "/lib/index.d.ts", 52 | 53 | /** 54 | * A list of NPM package names whose exports should be treated as part of this package. 55 | * 56 | * For example, suppose that Webpack is used to generate a distributed bundle for the project "library1", 57 | * and another NPM package "library2" is embedded in this bundle. Some types from library2 may become part 58 | * of the exported API for library1, but by default API Extractor would generate a .d.ts rollup that explicitly 59 | * imports library2. To avoid this, we can specify: 60 | * 61 | * "bundledPackages": [ "library2" ], 62 | * 63 | * This would direct API Extractor to embed those types directly in the .d.ts rollup, as if they had been 64 | * local files for library1. 65 | */ 66 | "bundledPackages": [], 67 | 68 | /** 69 | * Determines how the TypeScript compiler engine will be invoked by API Extractor. 70 | */ 71 | "compiler": { 72 | /** 73 | * Specifies the path to the tsconfig.json file to be used by API Extractor when analyzing the project. 74 | * 75 | * The path is resolved relative to the folder of the config file that contains the setting; to change this, 76 | * prepend a folder token such as "". 77 | * 78 | * Note: This setting will be ignored if "overrideTsconfig" is used. 79 | * 80 | * SUPPORTED TOKENS: , , 81 | * DEFAULT VALUE: "/tsconfig.json" 82 | */ 83 | // "tsconfigFilePath": "/tsconfig.json", 84 | /** 85 | * Provides a compiler configuration that will be used instead of reading the tsconfig.json file from disk. 86 | * The object must conform to the TypeScript tsconfig schema: 87 | * 88 | * http://json.schemastore.org/tsconfig 89 | * 90 | * If omitted, then the tsconfig.json file will be read from the "projectFolder". 91 | * 92 | * DEFAULT VALUE: no overrideTsconfig section 93 | */ 94 | // "overrideTsconfig": { 95 | // . . . 96 | // } 97 | /** 98 | * This option causes the compiler to be invoked with the --skipLibCheck option. This option is not recommended 99 | * and may cause API Extractor to produce incomplete or incorrect declarations, but it may be required when 100 | * dependencies contain declarations that are incompatible with the TypeScript engine that API Extractor uses 101 | * for its analysis. Where possible, the underlying issue should be fixed rather than relying on skipLibCheck. 102 | * 103 | * DEFAULT VALUE: false 104 | */ 105 | // "skipLibCheck": true, 106 | }, 107 | 108 | /** 109 | * Configures how the API report file (*.api.md) will be generated. 110 | */ 111 | "apiReport": { 112 | /** 113 | * (REQUIRED) Whether to generate an API report. 114 | */ 115 | "enabled": true 116 | 117 | /** 118 | * The filename for the API report files. It will be combined with "reportFolder" or "reportTempFolder" to produce 119 | * a full file path. 120 | * 121 | * The file extension should be ".api.md", and the string should not contain a path separator such as "\" or "/". 122 | * 123 | * SUPPORTED TOKENS: , 124 | * DEFAULT VALUE: ".api.md" 125 | */ 126 | // "reportFileName": ".api.md", 127 | 128 | /** 129 | * Specifies the folder where the API report file is written. The file name portion is determined by 130 | * the "reportFileName" setting. 131 | * 132 | * The API report file is normally tracked by Git. Changes to it can be used to trigger a branch policy, 133 | * e.g. for an API review. 134 | * 135 | * The path is resolved relative to the folder of the config file that contains the setting; to change this, 136 | * prepend a folder token such as "". 137 | * 138 | * SUPPORTED TOKENS: , , 139 | * DEFAULT VALUE: "/etc/" 140 | */ 141 | // "reportFolder": "/etc/", 142 | 143 | /** 144 | * Specifies the folder where the temporary report file is written. The file name portion is determined by 145 | * the "reportFileName" setting. 146 | * 147 | * After the temporary file is written to disk, it is compared with the file in the "reportFolder". 148 | * If they are different, a production build will fail. 149 | * 150 | * The path is resolved relative to the folder of the config file that contains the setting; to change this, 151 | * prepend a folder token such as "". 152 | * 153 | * SUPPORTED TOKENS: , , 154 | * DEFAULT VALUE: "/temp/" 155 | */ 156 | // "reportTempFolder": "/temp/" 157 | }, 158 | 159 | /** 160 | * Configures how the doc model file (*.api.json) will be generated. 161 | */ 162 | "docModel": { 163 | /** 164 | * (REQUIRED) Whether to generate a doc model file. 165 | */ 166 | "enabled": true, 167 | 168 | /** 169 | * The output path for the doc model file. The file extension should be ".api.json". 170 | * 171 | * The path is resolved relative to the folder of the config file that contains the setting; to change this, 172 | * prepend a folder token such as "". 173 | * 174 | * SUPPORTED TOKENS: , , 175 | * DEFAULT VALUE: "/temp/.api.json" 176 | */ 177 | "apiJsonFilePath": "/temp/api/.api.json" 178 | }, 179 | 180 | /** 181 | * Configures how the .d.ts rollup file will be generated. 182 | */ 183 | "dtsRollup": { 184 | /** 185 | * (REQUIRED) Whether to generate the .d.ts rollup file. 186 | */ 187 | "enabled": true 188 | 189 | /** 190 | * Specifies the output path for a .d.ts rollup file to be generated without any trimming. 191 | * This file will include all declarations that are exported by the main entry point. 192 | * 193 | * If the path is an empty string, then this file will not be written. 194 | * 195 | * The path is resolved relative to the folder of the config file that contains the setting; to change this, 196 | * prepend a folder token such as "". 197 | * 198 | * SUPPORTED TOKENS: , , 199 | * DEFAULT VALUE: "/dist/.d.ts" 200 | */ 201 | // "untrimmedFilePath": "/dist/.d.ts", 202 | 203 | /** 204 | * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "beta" release. 205 | * This file will include only declarations that are marked as "@public" or "@beta". 206 | * 207 | * The path is resolved relative to the folder of the config file that contains the setting; to change this, 208 | * prepend a folder token such as "". 209 | * 210 | * SUPPORTED TOKENS: , , 211 | * DEFAULT VALUE: "" 212 | */ 213 | // "betaTrimmedFilePath": "/dist/-beta.d.ts", 214 | 215 | /** 216 | * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "public" release. 217 | * This file will include only declarations that are marked as "@public". 218 | * 219 | * If the path is an empty string, then this file will not be written. 220 | * 221 | * The path is resolved relative to the folder of the config file that contains the setting; to change this, 222 | * prepend a folder token such as "". 223 | * 224 | * SUPPORTED TOKENS: , , 225 | * DEFAULT VALUE: "" 226 | */ 227 | // "publicTrimmedFilePath": "/dist/-public.d.ts", 228 | 229 | /** 230 | * When a declaration is trimmed, by default it will be replaced by a code comment such as 231 | * "Excluded from this release type: exampleMember". Set "omitTrimmingComments" to true to remove the 232 | * declaration completely. 233 | * 234 | * DEFAULT VALUE: false 235 | */ 236 | // "omitTrimmingComments": true 237 | }, 238 | 239 | /** 240 | * Configures how the tsdoc-metadata.json file will be generated. 241 | */ 242 | "tsdocMetadata": { 243 | /** 244 | * Whether to generate the tsdoc-metadata.json file. 245 | * 246 | * DEFAULT VALUE: true 247 | */ 248 | // "enabled": true, 249 | /** 250 | * Specifies where the TSDoc metadata file should be written. 251 | * 252 | * The path is resolved relative to the folder of the config file that contains the setting; to change this, 253 | * prepend a folder token such as "". 254 | * 255 | * The default value is "", which causes the path to be automatically inferred from the "tsdocMetadata", 256 | * "typings" or "main" fields of the project's package.json. If none of these fields are set, the lookup 257 | * falls back to "tsdoc-metadata.json" in the package folder. 258 | * 259 | * SUPPORTED TOKENS: , , 260 | * DEFAULT VALUE: "" 261 | */ 262 | // "tsdocMetadataFilePath": "/dist/tsdoc-metadata.json" 263 | }, 264 | 265 | /** 266 | * Specifies what type of newlines API Extractor should use when writing output files. By default, the output files 267 | * will be written with Windows-style newlines. To use POSIX-style newlines, specify "lf" instead. 268 | * To use the OS's default newline kind, specify "os". 269 | * 270 | * DEFAULT VALUE: "crlf" 271 | */ 272 | // "newlineKind": "crlf", 273 | 274 | /** 275 | * Configures how API Extractor reports error and warning messages produced during analysis. 276 | * 277 | * There are three sources of messages: compiler messages, API Extractor messages, and TSDoc messages. 278 | */ 279 | "messages": { 280 | /** 281 | * Configures handling of diagnostic messages reported by the TypeScript compiler engine while analyzing 282 | * the input .d.ts files. 283 | * 284 | * TypeScript message identifiers start with "TS" followed by an integer. For example: "TS2551" 285 | * 286 | * DEFAULT VALUE: A single "default" entry with logLevel=warning. 287 | */ 288 | "compilerMessageReporting": { 289 | /** 290 | * Configures the default routing for messages that don't match an explicit rule in this table. 291 | */ 292 | "default": { 293 | /** 294 | * Specifies whether the message should be written to the the tool's output log. Note that 295 | * the "addToApiReportFile" property may supersede this option. 296 | * 297 | * Possible values: "error", "warning", "none" 298 | * 299 | * Errors cause the build to fail and return a nonzero exit code. Warnings cause a production build fail 300 | * and return a nonzero exit code. For a non-production build (e.g. when "api-extractor run" includes 301 | * the "--local" option), the warning is displayed but the build will not fail. 302 | * 303 | * DEFAULT VALUE: "warning" 304 | */ 305 | "logLevel": "warning" 306 | 307 | /** 308 | * When addToApiReportFile is true: If API Extractor is configured to write an API report file (.api.md), 309 | * then the message will be written inside that file; otherwise, the message is instead logged according to 310 | * the "logLevel" option. 311 | * 312 | * DEFAULT VALUE: false 313 | */ 314 | // "addToApiReportFile": false 315 | } 316 | 317 | // "TS2551": { 318 | // "logLevel": "warning", 319 | // "addToApiReportFile": true 320 | // }, 321 | // 322 | // . . . 323 | }, 324 | 325 | /** 326 | * Configures handling of messages reported by API Extractor during its analysis. 327 | * 328 | * API Extractor message identifiers start with "ae-". For example: "ae-extra-release-tag" 329 | * 330 | * DEFAULT VALUE: See api-extractor-defaults.json for the complete table of extractorMessageReporting mappings 331 | */ 332 | "extractorMessageReporting": { 333 | "default": { 334 | "logLevel": "warning" 335 | // "addToApiReportFile": false 336 | } 337 | 338 | // "ae-extra-release-tag": { 339 | // "logLevel": "warning", 340 | // "addToApiReportFile": true 341 | // }, 342 | // 343 | // . . . 344 | }, 345 | 346 | /** 347 | * Configures handling of messages reported by the TSDoc parser when analyzing code comments. 348 | * 349 | * TSDoc message identifiers start with "tsdoc-". For example: "tsdoc-link-tag-unescaped-text" 350 | * 351 | * DEFAULT VALUE: A single "default" entry with logLevel=warning. 352 | */ 353 | "tsdocMessageReporting": { 354 | "default": { 355 | "logLevel": "warning" 356 | // "addToApiReportFile": false 357 | } 358 | 359 | // "tsdoc-link-tag-unescaped-text": { 360 | // "logLevel": "warning", 361 | // "addToApiReportFile": true 362 | // }, 363 | // 364 | // . . . 365 | } 366 | } 367 | } 368 | -------------------------------------------------------------------------------- /src/__snapshots__/generate.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`lets you post-process each operation, giving typed access to the meta 1`] = ` 4 | Object { 5 | "info": Object { 6 | "title": "tRPC HTTP-RPC", 7 | "version": "", 8 | }, 9 | "openapi": "3.0.0", 10 | "paths": Object { 11 | "/trpc/private": Object { 12 | "get": Object { 13 | "operationId": "private", 14 | "parameters": Array [ 15 | Object { 16 | "content": Object { 17 | "application/json": Object { 18 | "schema": Object { 19 | "additionalProperties": false, 20 | "properties": Object {}, 21 | "type": "object", 22 | }, 23 | }, 24 | }, 25 | "in": "query", 26 | "name": "input", 27 | }, 28 | ], 29 | "responses": Object { 30 | "200": Object { 31 | "description": "", 32 | }, 33 | }, 34 | "security": Array [ 35 | Object { 36 | "bearerAuth": Array [], 37 | }, 38 | ], 39 | "tags": Array [], 40 | }, 41 | }, 42 | "/trpc/public": Object { 43 | "get": Object { 44 | "operationId": "public", 45 | "parameters": Array [ 46 | Object { 47 | "content": Object { 48 | "application/json": Object { 49 | "schema": Object { 50 | "additionalProperties": false, 51 | "properties": Object {}, 52 | "type": "object", 53 | }, 54 | }, 55 | }, 56 | "in": "query", 57 | "name": "input", 58 | }, 59 | ], 60 | "responses": Object { 61 | "200": Object { 62 | "description": "", 63 | }, 64 | }, 65 | "tags": Array [], 66 | }, 67 | }, 68 | }, 69 | } 70 | `; 71 | 72 | exports[`works 1`] = ` 73 | Object { 74 | "info": Object { 75 | "title": "tRPC HTTP-RPC", 76 | "version": "", 77 | }, 78 | "openapi": "3.0.0", 79 | "paths": Object { 80 | "/trpc/dummy.hello.world": Object { 81 | "get": Object { 82 | "description": "ok", 83 | "operationId": "dummy.hello.world", 84 | "parameters": Array [ 85 | Object { 86 | "content": Object { 87 | "application/json": Object { 88 | "schema": Object { 89 | "additionalProperties": false, 90 | "properties": Object { 91 | "name": Object { 92 | "type": "string", 93 | }, 94 | }, 95 | "required": Array [ 96 | "name", 97 | ], 98 | "type": "object", 99 | }, 100 | }, 101 | }, 102 | "in": "query", 103 | "name": "input", 104 | }, 105 | ], 106 | "responses": Object { 107 | "200": Object { 108 | "content": Object { 109 | "application/json": Object { 110 | "schema": Object { 111 | "additionalProperties": false, 112 | "properties": Object { 113 | "result": Object { 114 | "additionalProperties": false, 115 | "properties": Object { 116 | "data": Object { 117 | "type": "string", 118 | }, 119 | }, 120 | "required": Array [ 121 | "data", 122 | ], 123 | "type": "object", 124 | }, 125 | }, 126 | "required": Array [ 127 | "result", 128 | ], 129 | "type": "object", 130 | }, 131 | }, 132 | }, 133 | "description": "", 134 | }, 135 | }, 136 | "tags": Array [ 137 | "dummy", 138 | ], 139 | }, 140 | }, 141 | "/trpc/example.createGreeting": Object { 142 | "post": Object { 143 | "operationId": "example.createGreeting", 144 | "requestBody": Object { 145 | "content": Object { 146 | "application/json": Object { 147 | "schema": Object { 148 | "additionalProperties": false, 149 | "description": "Input for createGreeting", 150 | "properties": Object { 151 | "name": Object { 152 | "description": "The name to greet", 153 | "type": "string", 154 | }, 155 | }, 156 | "type": "object", 157 | }, 158 | }, 159 | }, 160 | }, 161 | "responses": Object { 162 | "200": Object { 163 | "content": Object { 164 | "application/json": Object { 165 | "schema": Object { 166 | "additionalProperties": false, 167 | "properties": Object { 168 | "result": Object { 169 | "additionalProperties": false, 170 | "properties": Object { 171 | "data": Object { 172 | "description": "The greeting text", 173 | "type": "string", 174 | }, 175 | }, 176 | "required": Array [ 177 | "data", 178 | ], 179 | "type": "object", 180 | }, 181 | }, 182 | "required": Array [ 183 | "result", 184 | ], 185 | "type": "object", 186 | }, 187 | }, 188 | }, 189 | "description": "The greeting text", 190 | }, 191 | }, 192 | "summary": "Creates a greeting", 193 | "tags": Array [ 194 | "example", 195 | ], 196 | }, 197 | }, 198 | "/trpc/example.getAll": Object { 199 | "get": Object { 200 | "operationId": "example.getAll", 201 | "parameters": Array [ 202 | Object { 203 | "content": Object { 204 | "application/json": Object { 205 | "schema": Object { 206 | "additionalProperties": false, 207 | "properties": Object {}, 208 | "type": "object", 209 | }, 210 | }, 211 | }, 212 | "in": "query", 213 | "name": "input", 214 | }, 215 | ], 216 | "responses": Object { 217 | "200": Object { 218 | "content": Object { 219 | "application/json": Object { 220 | "schema": Object { 221 | "additionalProperties": false, 222 | "properties": Object { 223 | "result": Object { 224 | "additionalProperties": false, 225 | "properties": Object { 226 | "data": Object { 227 | "items": Object { 228 | "additionalProperties": false, 229 | "properties": Object { 230 | "id": Object { 231 | "type": "string", 232 | }, 233 | }, 234 | "required": Array [ 235 | "id", 236 | ], 237 | "type": "object", 238 | }, 239 | "type": "array", 240 | }, 241 | }, 242 | "required": Array [ 243 | "data", 244 | ], 245 | "type": "object", 246 | }, 247 | }, 248 | "required": Array [ 249 | "result", 250 | ], 251 | "type": "object", 252 | }, 253 | }, 254 | }, 255 | "description": "", 256 | }, 257 | }, 258 | "tags": Array [ 259 | "example", 260 | ], 261 | }, 262 | }, 263 | "/trpc/example.hello": Object { 264 | "get": Object { 265 | "operationId": "example.hello", 266 | "parameters": Array [ 267 | Object { 268 | "content": Object { 269 | "application/json": Object { 270 | "schema": Object { 271 | "additionalProperties": false, 272 | "properties": Object { 273 | "text": Object { 274 | "type": "string", 275 | }, 276 | }, 277 | "required": Array [ 278 | "text", 279 | ], 280 | "type": "object", 281 | }, 282 | }, 283 | }, 284 | "in": "query", 285 | "name": "input", 286 | }, 287 | ], 288 | "responses": Object { 289 | "200": Object { 290 | "description": "", 291 | }, 292 | }, 293 | "tags": Array [ 294 | "example", 295 | ], 296 | }, 297 | }, 298 | }, 299 | } 300 | `; 301 | 302 | exports[`works with array 1`] = ` 303 | Object { 304 | "info": Object { 305 | "title": "tRPC HTTP-RPC", 306 | "version": "", 307 | }, 308 | "openapi": "3.0.0", 309 | "paths": Object { 310 | "/trpc/dummy.hello.world": Object { 311 | "get": Object { 312 | "description": "ok", 313 | "operationId": "dummy.hello.world", 314 | "parameters": Array [ 315 | Object { 316 | "content": Object { 317 | "application/json": Object { 318 | "schema": Object { 319 | "additionalProperties": false, 320 | "properties": Object { 321 | "name": Object { 322 | "type": "string", 323 | }, 324 | }, 325 | "required": Array [ 326 | "name", 327 | ], 328 | "type": "object", 329 | }, 330 | }, 331 | }, 332 | "in": "query", 333 | "name": "input", 334 | }, 335 | ], 336 | "responses": Object { 337 | "200": Object { 338 | "content": Object { 339 | "application/json": Object { 340 | "schema": Object { 341 | "additionalProperties": false, 342 | "properties": Object { 343 | "result": Object { 344 | "additionalProperties": false, 345 | "properties": Object { 346 | "data": Object { 347 | "type": "string", 348 | }, 349 | }, 350 | "required": Array [ 351 | "data", 352 | ], 353 | "type": "object", 354 | }, 355 | }, 356 | "required": Array [ 357 | "result", 358 | ], 359 | "type": "object", 360 | }, 361 | }, 362 | }, 363 | "description": "", 364 | }, 365 | }, 366 | "tags": Array [ 367 | "dummy", 368 | ], 369 | }, 370 | }, 371 | "/trpc/exampleWithArrayAsInput.queryWithArrayInput": Object { 372 | "get": Object { 373 | "operationId": "exampleWithArrayAsInput.queryWithArrayInput", 374 | "parameters": Array [ 375 | Object { 376 | "content": Object { 377 | "application/json": Object { 378 | "schema": Object { 379 | "items": Object { 380 | "type": "string", 381 | }, 382 | "type": "array", 383 | }, 384 | }, 385 | }, 386 | "in": "query", 387 | "name": "input", 388 | }, 389 | ], 390 | "responses": Object { 391 | "200": Object { 392 | "description": "", 393 | }, 394 | }, 395 | "tags": Array [ 396 | "exampleWithArrayAsInput", 397 | ], 398 | }, 399 | }, 400 | }, 401 | } 402 | `; 403 | 404 | exports[`works with optional zod object 1`] = ` 405 | Object { 406 | "info": Object { 407 | "title": "tRPC HTTP-RPC", 408 | "version": "", 409 | }, 410 | "openapi": "3.0.0", 411 | "paths": Object { 412 | "/trpc/dummy.hello.world": Object { 413 | "get": Object { 414 | "description": "ok", 415 | "operationId": "dummy.hello.world", 416 | "parameters": Array [ 417 | Object { 418 | "content": Object { 419 | "application/json": Object { 420 | "schema": Object { 421 | "additionalProperties": false, 422 | "properties": Object { 423 | "name": Object { 424 | "type": "string", 425 | }, 426 | }, 427 | "required": Array [ 428 | "name", 429 | ], 430 | "type": "object", 431 | }, 432 | }, 433 | }, 434 | "in": "query", 435 | "name": "input", 436 | }, 437 | ], 438 | "responses": Object { 439 | "200": Object { 440 | "content": Object { 441 | "application/json": Object { 442 | "schema": Object { 443 | "additionalProperties": false, 444 | "properties": Object { 445 | "result": Object { 446 | "additionalProperties": false, 447 | "properties": Object { 448 | "data": Object { 449 | "type": "string", 450 | }, 451 | }, 452 | "required": Array [ 453 | "data", 454 | ], 455 | "type": "object", 456 | }, 457 | }, 458 | "required": Array [ 459 | "result", 460 | ], 461 | "type": "object", 462 | }, 463 | }, 464 | }, 465 | "description": "", 466 | }, 467 | }, 468 | "tags": Array [ 469 | "dummy", 470 | ], 471 | }, 472 | }, 473 | "/trpc/example.optionalObjectTest": Object { 474 | "get": Object { 475 | "operationId": "example.optionalObjectTest", 476 | "parameters": Array [ 477 | Object { 478 | "content": Object { 479 | "application/json": Object { 480 | "schema": Object { 481 | "anyOf": Array [ 482 | Object { 483 | "not": Object {}, 484 | }, 485 | Object { 486 | "additionalProperties": false, 487 | "properties": Object { 488 | "anOption": Object { 489 | "type": "string", 490 | }, 491 | }, 492 | "required": Array [ 493 | "anOption", 494 | ], 495 | "type": "object", 496 | }, 497 | ], 498 | }, 499 | }, 500 | }, 501 | "in": "query", 502 | "name": "input", 503 | }, 504 | ], 505 | "responses": Object { 506 | "200": Object { 507 | "description": "", 508 | }, 509 | }, 510 | "tags": Array [ 511 | "example", 512 | ], 513 | }, 514 | }, 515 | }, 516 | } 517 | `; 518 | --------------------------------------------------------------------------------