Skip to content

Commit 15044d4

Browse files
authored
feat!: migrate to @openapi-spec types and target OpenAPI 3.2 with a version option (#1998)
Replaces `json-schema-typed` and `@hey-api/spec-types` with `@openapi-spec/types`, so `JsonSchema` is the OpenAPI 3.2 Schema Object and `@orpc/openapi` re-exports the `OpenAPIV3_0`, `OpenAPIV3_1`, and `OpenAPIV3_2` namespaces. `OpenAPIGenerator` now builds OpenAPI 3.2 documents and takes a `version` option that downgrades to 3.1 or 3.0 through `@openapi-spec/downgrader`, with the return type following the requested version. ## Breaking changes - `generate()` emits `3.2.0` by default (was `3.1.2`). Pass `version: '3.1.2'` to keep 3.1 documents. `base` no longer accepts `openapi`. - `OpenAPIDocument` requires a version argument; `OpenAPIDocument<OpenAPIVersion>` is the union of every supported document. `OpenAPIOperationObject` is replaced by `OpenAPIV3_2.OperationObject`. - `JsonSchemaKeywords` is derived from the 3.2 Schema Object, so legacy keywords such as `additionalItems` are no longer part of it. ## Behavior - Any `3.0.x`, `3.1.x`, or `3.2.x` `version` is accepted. The minor version selects the conversion and the document carries the exact requested value. - Everything authored (`base`, `openapi({ spec })`, converter output) stays OpenAPI 3.2 and is downgraded with the rest of the document, so `type: ['string', 'null']` becomes `nullable: true` in 3.0 output. - Downgrading runs before serialization: the serializer output is not made of plain objects and the downgrader would pass it through untouched. - QUERY operations still require 3.2 and throw when an older version is requested. ## Docs - The specification page documents `version` in its own section, and the v1 migration guide notes the new default. ## Testing - Version tests compare whole output documents for `3.2.0`, `3.2.7`, `3.1.2`, `3.1.0`, `3.0.4`, and `3.0.0`, with type tests for the version-dependent return type. - Root type check, lint, the docs JSDoc backlink checker, and both package builds pass.
1 parent d7f6569 commit 15044d4

31 files changed

Lines changed: 358 additions & 152 deletions

apps/content/docs/migrations/from-v1.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1020,10 +1020,11 @@ const spec = await generator.generate(router, {
10201020

10211021
</CodeGroup>
10221022

1023-
Two smaller changes in the same area:
1023+
Three smaller changes in the same area:
10241024

10251025
- The `oo` helper (`oo.spec`) was removed. To customize the operation object, attach [`openapi({ spec })` metadata](/docs/openapi/specification#customizing-the-operation-object) directly on the procedure or router.
10261026
- The `shouldHoistDef` option was replaced by [`customComponentName`](/docs/openapi/specification#hoisting-defs). Root `$defs` are now always hoisted into `components.schemas`; this option only renames them.
1027+
- Documents are generated as OpenAPI 3.2.0 by default. Pass [`version: '3.1.1'`](/docs/openapi/specification#openapi-version) to keep old behavior.
10271028

10281029
### `OpenAPIReferencePlugin` renamed and reshaped
10291030

apps/content/docs/openapi/specification.mdx

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: "OpenAPI Specification"
3-
description: "Learn how to configure openapi metadata and generate OpenAPI 3.1 documents from your oRPC contracts and routers with OpenAPIGenerator."
3+
description: "Learn how to configure openapi metadata and generate OpenAPI 3.2, 3.1, or 3.0 documents from your oRPC contracts and routers with OpenAPIGenerator."
44
---
55

66
## Metadata
@@ -38,7 +38,7 @@ For routing metadata, you can learn more in [OpenAPI Routing](/docs/openapi/rout
3838

3939
### Customizing the Operation Object
4040

41-
Use `spec` to customize the generated operation object. If `spec` is an object, it replaces the generated operation object entirely. If `spec` is a callback, it receives the final operation object and returns an extended version.
41+
Use `spec` to customize the generated operation object. If `spec` is an object, it replaces the generated operation object entirely. If `spec` is a callback, it receives the final operation object and returns an extended version. The operation object always follows OpenAPI 3.2, whatever [version](#openapi-version) you generate.
4242

4343
```ts
4444
const getPlanet = oc
@@ -131,7 +131,7 @@ In this example, the final `tags` is `undefined`, so no tags are applied to `exa
131131

132132
## OpenAPI Generator
133133

134-
`OpenAPIGenerator` accepts either a [contract](/docs/contract/router) or a [router](/docs/router) and generates an OpenAPI 3.1 document by default. OpenAPI 3.2 is partially supported.
134+
`OpenAPIGenerator` turns a [contract](/docs/contract/router) or a [router](/docs/router) into an OpenAPI document.
135135

136136
```ts
137137
import { OpenAPIGenerator } from '@orpc/openapi'
@@ -141,6 +141,7 @@ const generator = new OpenAPIGenerator({
141141
})
142142

143143
const spec = await generator.generate(router, {
144+
version: '3.2.0',
144145
base: {
145146
info: {
146147
title: 'Planet API',
@@ -153,18 +154,24 @@ const spec = await generator.generate(router, {
153154
})
154155
```
155156

156-
### QUERY method
157+
`base` provides the OpenAPI 3.2 document fields to start from, such as `info`, `servers`, or `components`. The `openapi` field comes from `version`.
157158

158-
If your router contains a procedure that uses the `QUERY` method, explicitly set the OpenAPI version to `3.2.0`, because OpenAPI 3.1 does not support `QUERY`.
159+
### OpenAPI Version
160+
161+
`version` selects the OpenAPI version, `3.2.0` by default. Any `3.0.x`, `3.1.x`, or `3.2.x` value works.
159162

160163
```ts
161164
const spec = await generator.generate(router, {
162-
base: {
163-
openapi: '3.2.0',
164-
},
165+
version: '3.0.4',
165166
})
166167
```
167168

169+
The document is always built as OpenAPI 3.2, so `base`, [`openapi({ spec })`](#customizing-the-operation-object), and every JSON schema follow 3.2. Older versions come from downgrading the whole document with [`@openapi-spec/downgrader`](https://github.com/middleapi/openapi-spec/blob/main/packages/downgrader/README.md), which converts what the older version can still express and removes the rest.
170+
171+
:::warning
172+
`QUERY` operations require OpenAPI 3.2. Generating an older version from a router with a `QUERY` procedure throws.
173+
:::
174+
168175
### Json Schema Converters
169176

170177
`OpenAPIGenerator` relies on JSON Schema converters to translate your input, output, and error schemas into JSON Schemas. oRPC provides dedicated converters through the [Zod](/docs/integrations/zod), [Valibot](/docs/integrations/valibot), and [ArkType](/docs/integrations/arktype) integrations:

eslint.config.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,8 @@ export default antfu({
3939
'no-restricted-imports': ['error', {
4040
patterns: [{
4141
group: [
42-
'/json-schema-typed',
43-
'/openapi-types',
42+
'/@openapi-spec/types',
4443
'/@standard-schema/spec',
45-
'/@hey-api/spec-types',
4644
'/compression',
4745
],
4846
message: 'Please import from @orpc/* instead',

packages/json-schema/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,12 @@
4545
"type:check": "tsc -b"
4646
},
4747
"dependencies": {
48+
"@openapi-spec/types": "~0.1.0",
4849
"@orpc/client": "workspace:*",
4950
"@orpc/contract": "workspace:*",
5051
"@orpc/server": "workspace:*",
5152
"@orpc/shared": "workspace:*",
52-
"@standard-schema/spec": "^1.1.0",
53-
"json-schema-typed": "^8.0.2"
53+
"@standard-schema/spec": "^1.1.0"
5454
},
5555
"devDependencies": {
5656
"zod": "^4.5.4"

packages/json-schema/src/coercer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ export class JsonSchemaCoercer {
161161
: []
162162

163163
const itemSchema: JsonSchema | undefined = Array.isArray(schema.items)
164-
? schema.additionalItems
164+
? schema.additionalItems as JsonSchema | undefined
165165
: schema.items as JsonSchema | undefined
166166

167167
let shouldUseCoercedItems = false

packages/json-schema/src/composition-utils.test.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1548,15 +1548,16 @@ describe('combineJsonSchemasWithComposition', () => {
15481548
{
15491549
allOf: [{ $ref: '#/$defs/toString' }, { $ref: '#/$defs/valueOf' }],
15501550
$defs: {
1551-
toString: { type: 'string' },
1552-
constructor: { type: 'boolean' },
1551+
// Object.prototype member names get a method contextual type, `satisfies` keeps `type` literal
1552+
toString: { type: 'string' } satisfies JsonSchema,
1553+
constructor: { type: 'boolean' } satisfies JsonSchema,
15531554
},
15541555
},
15551556
{
15561557
$ref: '#/$defs/toString',
15571558
$defs: {
1558-
toString: { type: 'number' },
1559-
constructor: { type: 'boolean' },
1559+
toString: { type: 'number' } satisfies JsonSchema,
1560+
constructor: { type: 'boolean' } satisfies JsonSchema,
15601561
},
15611562
},
15621563
])).toEqual({
@@ -1581,7 +1582,7 @@ describe('combineJsonSchemasWithComposition', () => {
15811582
it('never lets two renames in the same branch land on the same target', () => {
15821583
// A skips the taken A2..A11 and lands on A12, which A1 would otherwise take as its own first choice
15831584
const taken = Object.fromEntries(
1584-
['A', 'A1', 'A2', 'A3', 'A4', 'A5', 'A6', 'A7', 'A8', 'A9', 'A10', 'A11'].map(name => [name, { type: 'string' }]),
1585+
['A', 'A1', 'A2', 'A3', 'A4', 'A5', 'A6', 'A7', 'A8', 'A9', 'A10', 'A11'].map((name): [string, JsonSchema] => [name, { type: 'string' }]),
15851586
)
15861587

15871588
expect(combineJsonSchemasWithComposition('anyOf', [

packages/json-schema/src/types.ts

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,54 @@
11
// eslint-disable-next-line no-restricted-imports
2-
import type * as Draft2020 from 'json-schema-typed/draft-2020-12'
2+
import type { OpenAPIV3_2 } from '@openapi-spec/types'
33

44
/**
55
* A JSON Schema (draft 2020-12) representation used across oRPC's JSON schema tooling.
66
*
7+
* @remarks
8+
* It is also the OpenAPI 3.2 Schema Object, so converted schemas embed into OpenAPI documents as-is.
9+
*
710
* @see {@link https://orpc.dev/docs/integrations/standard-schema | Standard Schema Integration}
811
*/
9-
export type JsonSchema<Value = any> = Draft2020.JSONSchema<Value>
10-
export type JsonSchemaKeywords = typeof Draft2020.keywords[number]
12+
export type JsonSchema<Value = any> = OpenAPIV3_2.SchemaObject<Value>
13+
14+
/**
15+
* The declared JSON Schema keywords, the index signature is filtered out.
16+
*/
17+
export type JsonSchemaKeywords = keyof {
18+
[K in keyof OpenAPIV3_2.SchemaObjectFields as string extends K ? never : K]: unknown
19+
}
20+
21+
export enum JsonSchemaType {
22+
Array = 'array',
23+
Boolean = 'boolean',
24+
Integer = 'integer',
25+
Null = 'null',
26+
Number = 'number',
27+
Object = 'object',
28+
String = 'string',
29+
}
30+
31+
export enum JsonSchemaFormat {
32+
Date = 'date',
33+
DateTime = 'date-time',
34+
Duration = 'duration',
35+
Email = 'email',
36+
Hostname = 'hostname',
37+
IDNEmail = 'idn-email',
38+
IDNHostname = 'idn-hostname',
39+
IPv4 = 'ipv4',
40+
IPv6 = 'ipv6',
41+
IRI = 'iri',
42+
IRIReference = 'iri-reference',
43+
JSONPointer = 'json-pointer',
44+
RegEx = 'regex',
45+
RelativeJSONPointer = 'relative-json-pointer',
46+
Time = 'time',
47+
URI = 'uri',
48+
URIReference = 'uri-reference',
49+
URITemplate = 'uri-template',
50+
UUID = 'uuid',
51+
}
1152

1253
export enum JsonSchemaXNativeType {
1354
BigInt = 'bigint',
@@ -17,6 +58,3 @@ export enum JsonSchemaXNativeType {
1758
Set = 'set',
1859
Map = 'map',
1960
}
20-
21-
// eslint-disable-next-line no-restricted-imports
22-
export { Format as JsonSchemaFormat, TypeName as JsonSchemaType } from 'json-schema-typed/draft-2020-12'

packages/openapi/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,8 @@
115115
}
116116
},
117117
"dependencies": {
118-
"@hey-api/spec-types": "0.0.0-next-20260408030107",
118+
"@openapi-spec/downgrader": "~0.1.0",
119+
"@openapi-spec/types": "~0.1.0",
119120
"@orpc/client": "workspace:*",
120121
"@orpc/contract": "workspace:*",
121122
"@orpc/json-schema": "workspace:*",

packages/openapi/src/meta.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { AnyProcedureContract, AnySchema, ErrorMap, MetaPlugin } from '@orp
22
import type { Lazy } from '@orpc/server'
33
import type { Value } from '@orpc/shared'
44
import type { StandardBodyHint } from '@standard-server/core'
5-
import type { OpenAPIOperationObject } from './types'
5+
import type { OpenAPIV3_2 } from './types'
66
import { mergeHttpPath } from '@orpc/shared'
77

88
export interface OpenAPIMeta {
@@ -289,6 +289,9 @@ export interface OpenAPIMeta {
289289
* Pass a plain object to replace entire operation object, or a function that receives the current
290290
* operation object and returns the modified version.
291291
*
292+
* The operation object always follows OpenAPI 3.2, `OpenAPIGenerator` downgrades it with the
293+
* rest of the document when an older version is requested.
294+
*
292295
* **Merging**: When defined multiple times:
293296
*
294297
* - Two functions are chained: the most recent function receives the result of the previous one.
@@ -297,7 +300,7 @@ export interface OpenAPIMeta {
297300
*
298301
* Explicitly setting `undefined` resets the spec instead of merging.
299302
*/
300-
spec?: Value<OpenAPIOperationObject, [current: OpenAPIOperationObject]>
303+
spec?: Value<OpenAPIV3_2.OperationObject, [current: OpenAPIV3_2.OperationObject]>
301304

302305
/**
303306
* Prefix for the path. Useful when you want to apply a common path prefix across multiple procedures.

packages/openapi/src/openapi-generator-components.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import type { JsonSchema } from '@orpc/json-schema'
2-
import type { OpenAPIDocument } from './types'
2+
import type { OpenAPIV3_2 } from './types'
33
import { OpenAPIComponentRegistry } from './openapi-generator-components'
44

55
describe('openAPIComponentRegistry', () => {
66
function createRegistry(options: {
77
schemas?: Record<string, any>
88
customComponentName?: (defName: string, defSchema: JsonSchema) => string | undefined
99
} = {}) {
10-
const doc: OpenAPIDocument = {
11-
openapi: '3.1.2',
10+
const doc: OpenAPIV3_2.OpenAPIObject = {
11+
openapi: '3.2.0',
1212
info: { title: 'API Reference', version: '0.0.0' },
1313
...(options.schemas ? { components: { schemas: options.schemas } } : {}),
1414
}

0 commit comments

Comments
 (0)