@orpc/openapi 0.0.0-next.f50512c → 0.0.0-next.f5149a8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +7 -2
  2. package/dist/adapters/aws-lambda/index.d.mts +2 -1
  3. package/dist/adapters/aws-lambda/index.d.ts +2 -1
  4. package/dist/adapters/aws-lambda/index.mjs +1 -1
  5. package/dist/adapters/fastify/index.d.mts +23 -0
  6. package/dist/adapters/fastify/index.d.ts +23 -0
  7. package/dist/adapters/fastify/index.mjs +18 -0
  8. package/dist/adapters/fetch/index.d.mts +7 -3
  9. package/dist/adapters/fetch/index.d.ts +7 -3
  10. package/dist/adapters/fetch/index.mjs +1 -1
  11. package/dist/adapters/node/index.d.mts +7 -3
  12. package/dist/adapters/node/index.d.ts +7 -3
  13. package/dist/adapters/node/index.mjs +1 -1
  14. package/dist/adapters/standard/index.d.mts +7 -16
  15. package/dist/adapters/standard/index.d.ts +7 -16
  16. package/dist/adapters/standard/index.mjs +1 -1
  17. package/dist/index.d.mts +10 -4
  18. package/dist/index.d.ts +10 -4
  19. package/dist/index.mjs +3 -3
  20. package/dist/plugins/index.d.mts +19 -4
  21. package/dist/plugins/index.d.ts +19 -4
  22. package/dist/plugins/index.mjs +59 -19
  23. package/dist/shared/{openapi.DPAN3GVs.d.mts → openapi.BfNjg7j9.d.mts} +14 -2
  24. package/dist/shared/{openapi.DPAN3GVs.d.ts → openapi.BfNjg7j9.d.ts} +14 -2
  25. package/dist/shared/{openapi.BtoY8ZFF.mjs → openapi.CzHcOMxv.mjs} +155 -44
  26. package/dist/shared/{openapi.BkBlt1Qf.mjs → openapi.DIt-Z9W1.mjs} +8 -5
  27. package/dist/shared/openapi.DwaweYRb.d.mts +54 -0
  28. package/dist/shared/openapi.DwaweYRb.d.ts +54 -0
  29. package/package.json +16 -10
  30. package/dist/shared/openapi.CQmjvnb0.d.mts +0 -31
  31. package/dist/shared/openapi.CQmjvnb0.d.ts +0 -31
@@ -1,11 +1,11 @@
1
1
  import { stringifyJSON, once, value } from '@orpc/shared';
2
- import { O as OpenAPIGenerator } from '../shared/openapi.BtoY8ZFF.mjs';
2
+ import { O as OpenAPIGenerator } from '../shared/openapi.CzHcOMxv.mjs';
3
3
  import '@orpc/client';
4
4
  import '@orpc/client/standard';
5
5
  import '@orpc/contract';
6
6
  import '@orpc/openapi-client/standard';
7
7
  import '@orpc/server';
8
- import 'json-schema-typed/draft-2020-12';
8
+ import '@orpc/interop/json-schema-typed/draft-2020-12';
9
9
 
10
10
  class OpenAPIReferencePlugin {
11
11
  generator;
@@ -14,7 +14,9 @@ class OpenAPIReferencePlugin {
14
14
  docsPath;
15
15
  docsTitle;
16
16
  docsHead;
17
+ docsProvider;
17
18
  docsScriptUrl;
19
+ docsCssUrl;
18
20
  docsConfig;
19
21
  renderDocsHtml;
20
22
  constructor(options = {}) {
@@ -22,16 +24,59 @@ class OpenAPIReferencePlugin {
22
24
  this.docsPath = options.docsPath ?? "/";
23
25
  this.docsTitle = options.docsTitle ?? "API Reference";
24
26
  this.docsConfig = options.docsConfig ?? void 0;
25
- this.docsScriptUrl = options.docsScriptUrl ?? "https://cdn.jsdelivr.net/npm/@scalar/api-reference";
27
+ this.docsProvider = options.docsProvider ?? "scalar";
28
+ this.docsScriptUrl = options.docsScriptUrl ?? (this.docsProvider === "swagger" ? "https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js" : "https://cdn.jsdelivr.net/npm/@scalar/api-reference");
29
+ this.docsCssUrl = options.docsCssUrl ?? (this.docsProvider === "swagger" ? "https://unpkg.com/swagger-ui-dist/swagger-ui.css" : void 0);
26
30
  this.docsHead = options.docsHead ?? "";
27
31
  this.specPath = options.specPath ?? "/spec.json";
28
32
  this.generator = new OpenAPIGenerator(options);
29
33
  const esc = (s) => s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
30
- this.renderDocsHtml = options.renderDocsHtml ?? ((specUrl, title, head, scriptUrl, config, spec) => {
31
- const finalConfig = {
32
- content: stringifyJSON(spec),
33
- ...config
34
- };
34
+ this.renderDocsHtml = options.renderDocsHtml ?? ((specUrl, title, head, scriptUrl, config, spec, docsProvider, cssUrl) => {
35
+ let body;
36
+ if (docsProvider === "swagger") {
37
+ const swaggerConfig = {
38
+ dom_id: "#app",
39
+ spec,
40
+ deepLinking: true,
41
+ presets: [
42
+ "SwaggerUIBundle.presets.apis",
43
+ "SwaggerUIBundle.presets.standalone"
44
+ ],
45
+ plugins: [
46
+ "SwaggerUIBundle.plugins.DownloadUrl"
47
+ ],
48
+ ...config
49
+ };
50
+ body = `
51
+ <body>
52
+ <div id="app"></div>
53
+
54
+ <script src="${esc(scriptUrl)}"><\/script>
55
+
56
+ <script>
57
+ window.onload = () => {
58
+ window.ui = SwaggerUIBundle(${stringifyJSON(swaggerConfig).replace(/"(SwaggerUIBundle\.[^"]+)"/g, "$1")})
59
+ }
60
+ <\/script>
61
+ </body>
62
+ `;
63
+ } else {
64
+ const scalarConfig = {
65
+ content: stringifyJSON(spec),
66
+ ...config
67
+ };
68
+ body = `
69
+ <body>
70
+ <div id="app" data-config="${esc(stringifyJSON(scalarConfig))}"></div>
71
+
72
+ <script src="${esc(scriptUrl)}"><\/script>
73
+
74
+ <script>
75
+ Scalar.createApiReference('#app', JSON.parse(document.getElementById('app').dataset.config))
76
+ <\/script>
77
+ </body>
78
+ `;
79
+ }
35
80
  return `
36
81
  <!doctype html>
37
82
  <html>
@@ -39,19 +84,12 @@ class OpenAPIReferencePlugin {
39
84
  <meta charset="utf-8" />
40
85
  <meta name="viewport" content="width=device-width, initial-scale=1" />
41
86
  <title>${esc(title)}</title>
87
+ ${cssUrl ? `<link rel="stylesheet" type="text/css" href="${esc(cssUrl)}" />` : ""}
42
88
  ${head}
43
89
  </head>
44
- <body>
45
- <div id="app" data-config="${esc(stringifyJSON(finalConfig))}"></div>
46
-
47
- <script src="${esc(scriptUrl)}"><\/script>
48
-
49
- <script>
50
- Scalar.createApiReference('#app', JSON.parse(document.getElementById('app').dataset.config))
51
- <\/script>
52
- </body>
90
+ ${body}
53
91
  </html>
54
- `;
92
+ `;
55
93
  });
56
94
  }
57
95
  init(options, router) {
@@ -89,7 +127,9 @@ class OpenAPIReferencePlugin {
89
127
  await value(this.docsHead, options2),
90
128
  await value(this.docsScriptUrl, options2),
91
129
  await value(this.docsConfig, options2),
92
- await generateSpec()
130
+ await generateSpec(),
131
+ this.docsProvider,
132
+ await value(this.docsCssUrl, options2)
93
133
  );
94
134
  return {
95
135
  matched: true,
@@ -2,7 +2,7 @@ import { AnySchema, OpenAPI, AnyContractProcedure, AnyContractRouter } from '@or
2
2
  import { StandardOpenAPIJsonSerializerOptions } from '@orpc/openapi-client/standard';
3
3
  import { AnyProcedure, TraverseContractProcedureCallbackOptions, AnyRouter } from '@orpc/server';
4
4
  import { Promisable, Value } from '@orpc/shared';
5
- import { JSONSchema } from 'json-schema-typed/draft-2020-12';
5
+ import { JSONSchema } from '@orpc/interop/json-schema-typed/draft-2020-12';
6
6
 
7
7
  interface SchemaConverterComponent {
8
8
  allowedStrategies: readonly SchemaConvertOptions['strategy'][];
@@ -85,6 +85,18 @@ interface OpenAPIGeneratorGenerateOptions extends Partial<Omit<OpenAPI.Document,
85
85
  error: 'UndefinedError';
86
86
  schema?: never;
87
87
  }>;
88
+ /**
89
+ * Define a custom JSON schema for the error response body when using
90
+ * type-safe errors. Helps align ORPC error formatting with existing API
91
+ * response standards or conventions.
92
+ *
93
+ * @remarks
94
+ * - Return `null | undefined` to use the default error response body shaper.
95
+ */
96
+ customErrorResponseBodySchema?: Value<JSONSchema | undefined | null, [
97
+ definedErrors: [code: string, defaultMessage: string, dataRequired: boolean, dataSchema: JSONSchema][],
98
+ status: number
99
+ ]>;
88
100
  }
89
101
  /**
90
102
  * The generator that converts oRPC routers/contracts to OpenAPI specifications.
@@ -101,7 +113,7 @@ declare class OpenAPIGenerator {
101
113
  *
102
114
  * @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
103
115
  */
104
- generate(router: AnyContractRouter | AnyRouter, options?: OpenAPIGeneratorGenerateOptions): Promise<OpenAPI.Document>;
116
+ generate(router: AnyContractRouter | AnyRouter, { customErrorResponseBodySchema, commonSchemas, filter: baseFilter, exclude, ...baseDoc }?: OpenAPIGeneratorGenerateOptions): Promise<OpenAPI.Document>;
105
117
  }
106
118
 
107
119
  export { OpenAPIGenerator as b, CompositeSchemaConverter as e };
@@ -2,7 +2,7 @@ import { AnySchema, OpenAPI, AnyContractProcedure, AnyContractRouter } from '@or
2
2
  import { StandardOpenAPIJsonSerializerOptions } from '@orpc/openapi-client/standard';
3
3
  import { AnyProcedure, TraverseContractProcedureCallbackOptions, AnyRouter } from '@orpc/server';
4
4
  import { Promisable, Value } from '@orpc/shared';
5
- import { JSONSchema } from 'json-schema-typed/draft-2020-12';
5
+ import { JSONSchema } from '@orpc/interop/json-schema-typed/draft-2020-12';
6
6
 
7
7
  interface SchemaConverterComponent {
8
8
  allowedStrategies: readonly SchemaConvertOptions['strategy'][];
@@ -85,6 +85,18 @@ interface OpenAPIGeneratorGenerateOptions extends Partial<Omit<OpenAPI.Document,
85
85
  error: 'UndefinedError';
86
86
  schema?: never;
87
87
  }>;
88
+ /**
89
+ * Define a custom JSON schema for the error response body when using
90
+ * type-safe errors. Helps align ORPC error formatting with existing API
91
+ * response standards or conventions.
92
+ *
93
+ * @remarks
94
+ * - Return `null | undefined` to use the default error response body shaper.
95
+ */
96
+ customErrorResponseBodySchema?: Value<JSONSchema | undefined | null, [
97
+ definedErrors: [code: string, defaultMessage: string, dataRequired: boolean, dataSchema: JSONSchema][],
98
+ status: number
99
+ ]>;
88
100
  }
89
101
  /**
90
102
  * The generator that converts oRPC routers/contracts to OpenAPI specifications.
@@ -101,7 +113,7 @@ declare class OpenAPIGenerator {
101
113
  *
102
114
  * @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
103
115
  */
104
- generate(router: AnyContractRouter | AnyRouter, options?: OpenAPIGeneratorGenerateOptions): Promise<OpenAPI.Document>;
116
+ generate(router: AnyContractRouter | AnyRouter, { customErrorResponseBodySchema, commonSchemas, filter: baseFilter, exclude, ...baseDoc }?: OpenAPIGeneratorGenerateOptions): Promise<OpenAPI.Document>;
105
117
  }
106
118
 
107
119
  export { OpenAPIGenerator as b, CompositeSchemaConverter as e };
@@ -4,7 +4,7 @@ import { fallbackContractConfig, getEventIteratorSchemaDetails } from '@orpc/con
4
4
  import { standardizeHTTPPath, StandardOpenAPIJsonSerializer, getDynamicParams } from '@orpc/openapi-client/standard';
5
5
  import { isProcedure, resolveContractProcedures } from '@orpc/server';
6
6
  import { isObject, stringifyJSON, findDeepMatches, toArray, clone, value } from '@orpc/shared';
7
- import { TypeName } from 'json-schema-typed/draft-2020-12';
7
+ import { TypeName } from '@orpc/interop/json-schema-typed/draft-2020-12';
8
8
 
9
9
  const OPERATION_EXTENDER_SYMBOL = Symbol("ORPC_OPERATION_EXTENDER");
10
10
  function customOpenAPIOperation(o, extend) {
@@ -114,13 +114,18 @@ function isAnySchema(schema) {
114
114
  return false;
115
115
  }
116
116
  function separateObjectSchema(schema, separatedProperties) {
117
- if (Object.keys(schema).some((k) => k !== "type" && k !== "properties" && k !== "required" && LOGIC_KEYWORDS.includes(k))) {
117
+ if (Object.keys(schema).some(
118
+ (k) => !["type", "properties", "required", "additionalProperties"].includes(k) && LOGIC_KEYWORDS.includes(k) && schema[k] !== void 0
119
+ )) {
118
120
  return [{ type: "object" }, schema];
119
121
  }
120
122
  const matched = { ...schema };
121
123
  const rest = { ...schema };
122
- matched.properties = schema.properties && Object.entries(schema.properties).filter(([key]) => separatedProperties.includes(key)).reduce((acc, [key, value]) => {
123
- acc[key] = value;
124
+ matched.properties = separatedProperties.reduce((acc, key) => {
125
+ const keySchema = schema.properties?.[key] ?? schema.additionalProperties;
126
+ if (keySchema !== void 0) {
127
+ acc[key] = keySchema;
128
+ }
124
129
  return acc;
125
130
  }, {});
126
131
  matched.required = schema.required?.filter((key) => separatedProperties.includes(key));
@@ -354,6 +359,107 @@ function resolveOpenAPIJsonSchemaRef(doc, schema) {
354
359
  const resolved = doc.components?.schemas?.[name];
355
360
  return resolved ?? schema;
356
361
  }
362
+ function simplifyComposedObjectJsonSchemasAndRefs(schema, doc) {
363
+ if (doc) {
364
+ schema = resolveOpenAPIJsonSchemaRef(doc, schema);
365
+ }
366
+ if (typeof schema !== "object" || !schema.anyOf && !schema.oneOf && !schema.allOf) {
367
+ return schema;
368
+ }
369
+ const unionSchemas = [
370
+ ...toArray(schema.anyOf?.map((s) => simplifyComposedObjectJsonSchemasAndRefs(s, doc))),
371
+ ...toArray(schema.oneOf?.map((s) => simplifyComposedObjectJsonSchemasAndRefs(s, doc)))
372
+ ];
373
+ const objectUnionSchemas = [];
374
+ for (const u of unionSchemas) {
375
+ if (!isObjectSchema(u)) {
376
+ return schema;
377
+ }
378
+ objectUnionSchemas.push(u);
379
+ }
380
+ const mergedUnionPropertyMap = /* @__PURE__ */ new Map();
381
+ for (const u of objectUnionSchemas) {
382
+ if (u.properties) {
383
+ for (const [key, value] of Object.entries(u.properties)) {
384
+ let entry = mergedUnionPropertyMap.get(key);
385
+ if (!entry) {
386
+ const required = objectUnionSchemas.every((s) => s.required?.includes(key));
387
+ entry = { required, schemas: [] };
388
+ mergedUnionPropertyMap.set(key, entry);
389
+ }
390
+ entry.schemas.push(value);
391
+ }
392
+ }
393
+ }
394
+ const intersectionSchemas = toArray(schema.allOf?.map((s) => simplifyComposedObjectJsonSchemasAndRefs(s, doc)));
395
+ const objectIntersectionSchemas = [];
396
+ for (const u of intersectionSchemas) {
397
+ if (!isObjectSchema(u)) {
398
+ return schema;
399
+ }
400
+ objectIntersectionSchemas.push(u);
401
+ }
402
+ if (isObjectSchema(schema)) {
403
+ objectIntersectionSchemas.push(schema);
404
+ }
405
+ const mergedInteractionPropertyMap = /* @__PURE__ */ new Map();
406
+ for (const u of objectIntersectionSchemas) {
407
+ if (u.properties) {
408
+ for (const [key, value] of Object.entries(u.properties)) {
409
+ let entry = mergedInteractionPropertyMap.get(key);
410
+ if (!entry) {
411
+ const required = objectIntersectionSchemas.some((s) => s.required?.includes(key));
412
+ entry = { required, schemas: [] };
413
+ mergedInteractionPropertyMap.set(key, entry);
414
+ }
415
+ entry.schemas.push(value);
416
+ }
417
+ }
418
+ }
419
+ const resultObjectSchema = { type: "object", properties: {}, required: [] };
420
+ const keys = /* @__PURE__ */ new Set([
421
+ ...mergedUnionPropertyMap.keys(),
422
+ ...mergedInteractionPropertyMap.keys()
423
+ ]);
424
+ if (keys.size === 0) {
425
+ return schema;
426
+ }
427
+ const deduplicateSchemas = (schemas) => {
428
+ const seen = /* @__PURE__ */ new Set();
429
+ const result = [];
430
+ for (const schema2 of schemas) {
431
+ const key = stringifyJSON(schema2);
432
+ if (!seen.has(key)) {
433
+ seen.add(key);
434
+ result.push(schema2);
435
+ }
436
+ }
437
+ return result;
438
+ };
439
+ for (const key of keys) {
440
+ const unionEntry = mergedUnionPropertyMap.get(key);
441
+ const intersectionEntry = mergedInteractionPropertyMap.get(key);
442
+ resultObjectSchema.properties[key] = (() => {
443
+ const dedupedUnionSchemas = unionEntry ? deduplicateSchemas(unionEntry.schemas) : [];
444
+ const dedupedIntersectionSchemas = intersectionEntry ? deduplicateSchemas(intersectionEntry.schemas) : [];
445
+ if (!dedupedUnionSchemas.length) {
446
+ return dedupedIntersectionSchemas.length === 1 ? dedupedIntersectionSchemas[0] : { allOf: dedupedIntersectionSchemas };
447
+ }
448
+ if (!dedupedIntersectionSchemas.length) {
449
+ return dedupedUnionSchemas.length === 1 ? dedupedUnionSchemas[0] : { anyOf: dedupedUnionSchemas };
450
+ }
451
+ const allOf = deduplicateSchemas([
452
+ ...dedupedIntersectionSchemas,
453
+ dedupedUnionSchemas.length === 1 ? dedupedUnionSchemas[0] : { anyOf: dedupedUnionSchemas }
454
+ ]);
455
+ return allOf.length === 1 ? allOf[0] : { allOf };
456
+ })();
457
+ if (unionEntry?.required || intersectionEntry?.required) {
458
+ resultObjectSchema.required.push(key);
459
+ }
460
+ }
461
+ return resultObjectSchema;
462
+ }
357
463
 
358
464
  class CompositeSchemaConverter {
359
465
  converters;
@@ -384,19 +490,16 @@ class OpenAPIGenerator {
384
490
  *
385
491
  * @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
386
492
  */
387
- async generate(router, options = {}) {
388
- const filter = options.filter ?? (({ contract, path }) => {
389
- return !(options.exclude?.(contract, path) ?? false);
493
+ async generate(router, { customErrorResponseBodySchema, commonSchemas, filter: baseFilter, exclude, ...baseDoc } = {}) {
494
+ const filter = baseFilter ?? (({ contract, path }) => {
495
+ return !(exclude?.(contract, path) ?? false);
390
496
  });
391
497
  const doc = {
392
- ...clone(options),
393
- info: options.info ?? { title: "API Reference", version: "0.0.0" },
394
- openapi: "3.1.1",
395
- exclude: void 0,
396
- filter: void 0,
397
- commonSchemas: void 0
498
+ ...clone(baseDoc),
499
+ info: baseDoc.info ?? { title: "API Reference", version: "0.0.0" },
500
+ openapi: "3.1.1"
398
501
  };
399
- const { baseSchemaConvertOptions, undefinedErrorJsonSchema } = await this.#resolveCommonSchemas(doc, options.commonSchemas);
502
+ const { baseSchemaConvertOptions, undefinedErrorJsonSchema } = await this.#resolveCommonSchemas(doc, commonSchemas);
400
503
  const contracts = [];
401
504
  await resolveContractProcedures({ path: [], router }, (traverseOptions) => {
402
505
  if (!value(filter, traverseOptions)) {
@@ -406,17 +509,17 @@ class OpenAPIGenerator {
406
509
  });
407
510
  const errors = [];
408
511
  for (const { contract, path } of contracts) {
409
- const operationId = path.join(".");
512
+ const stringPath = path.join(".");
410
513
  try {
411
514
  const def = contract["~orpc"];
412
515
  const method = toOpenAPIMethod(fallbackContractConfig("defaultMethod", def.route.method));
413
516
  const httpPath = toOpenAPIPath(def.route.path ?? toHttpPath(path));
414
517
  let operationObjectRef;
415
- if (def.route.spec !== void 0) {
518
+ if (def.route.spec !== void 0 && typeof def.route.spec !== "function") {
416
519
  operationObjectRef = def.route.spec;
417
520
  } else {
418
521
  operationObjectRef = {
419
- operationId,
522
+ operationId: def.route.operationId ?? stringPath,
420
523
  summary: def.route.summary,
421
524
  description: def.route.description,
422
525
  deprecated: def.route.deprecated,
@@ -424,7 +527,10 @@ class OpenAPIGenerator {
424
527
  };
425
528
  await this.#request(doc, operationObjectRef, def, baseSchemaConvertOptions);
426
529
  await this.#successResponse(doc, operationObjectRef, def, baseSchemaConvertOptions);
427
- await this.#errorResponse(operationObjectRef, def, baseSchemaConvertOptions, undefinedErrorJsonSchema);
530
+ await this.#errorResponse(operationObjectRef, def, baseSchemaConvertOptions, undefinedErrorJsonSchema, customErrorResponseBodySchema);
531
+ }
532
+ if (typeof def.route.spec === "function") {
533
+ operationObjectRef = def.route.spec(operationObjectRef);
428
534
  }
429
535
  doc.paths ??= {};
430
536
  doc.paths[httpPath] ??= {};
@@ -434,7 +540,7 @@ class OpenAPIGenerator {
434
540
  throw e;
435
541
  }
436
542
  errors.push(
437
- `[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${operationId}
543
+ `[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${stringPath}
438
544
  ${e.message}`
439
545
  );
440
546
  }
@@ -534,13 +640,15 @@ ${errors.join("\n\n")}`
534
640
  def.inputSchema,
535
641
  {
536
642
  ...baseSchemaConvertOptions,
537
- strategy: "input",
538
- minStructureDepthForRef: dynamicParams?.length || inputStructure === "detailed" ? 1 : 0
643
+ strategy: "input"
539
644
  }
540
645
  );
541
646
  if (isAnySchema(schema) && !dynamicParams?.length) {
542
647
  return;
543
648
  }
649
+ if (inputStructure === "detailed" || inputStructure === "compact" && (dynamicParams?.length || method === "GET")) {
650
+ schema = simplifyComposedObjectJsonSchemasAndRefs(schema, doc);
651
+ }
544
652
  if (inputStructure === "compact") {
545
653
  if (dynamicParams?.length) {
546
654
  const error2 = new OpenAPIGeneratorError(
@@ -580,7 +688,7 @@ ${errors.join("\n\n")}`
580
688
  if (!isObjectSchema(schema)) {
581
689
  throw error;
582
690
  }
583
- const resolvedParamSchema = schema.properties?.params !== void 0 ? resolveOpenAPIJsonSchemaRef(doc, schema.properties.params) : void 0;
691
+ const resolvedParamSchema = schema.properties?.params !== void 0 ? simplifyComposedObjectJsonSchemasAndRefs(schema.properties.params, doc) : void 0;
584
692
  if (dynamicParams?.length && (resolvedParamSchema === void 0 || !isObjectSchema(resolvedParamSchema) || !checkParamsSchema(resolvedParamSchema, dynamicParams))) {
585
693
  throw new OpenAPIGeneratorError(
586
694
  'When input structure is "detailed" and path has dynamic params, the "params" schema must be an object with all dynamic params as required.'
@@ -589,7 +697,7 @@ ${errors.join("\n\n")}`
589
697
  for (const from of ["params", "query", "headers"]) {
590
698
  const fromSchema = schema.properties?.[from];
591
699
  if (fromSchema !== void 0) {
592
- const resolvedSchema = resolveOpenAPIJsonSchemaRef(doc, fromSchema);
700
+ const resolvedSchema = simplifyComposedObjectJsonSchemasAndRefs(fromSchema, doc);
593
701
  if (!isObjectSchema(resolvedSchema)) {
594
702
  throw error;
595
703
  }
@@ -650,13 +758,14 @@ ${errors.join("\n\n")}`
650
758
 
651
759
  But got: ${stringifyJSON(item)}
652
760
  `);
653
- if (!isObjectSchema(item)) {
761
+ const simplifiedItem = simplifyComposedObjectJsonSchemasAndRefs(item, doc);
762
+ if (!isObjectSchema(simplifiedItem)) {
654
763
  throw error;
655
764
  }
656
765
  let schemaStatus;
657
766
  let schemaDescription;
658
- if (item.properties?.status !== void 0) {
659
- const statusSchema = resolveOpenAPIJsonSchemaRef(doc, item.properties.status);
767
+ if (simplifiedItem.properties?.status !== void 0) {
768
+ const statusSchema = resolveOpenAPIJsonSchemaRef(doc, simplifiedItem.properties.status);
660
769
  if (typeof statusSchema !== "object" || statusSchema.const === void 0 || typeof statusSchema.const !== "number" || !Number.isInteger(statusSchema.const) || isORPCErrorStatus(statusSchema.const)) {
661
770
  throw error;
662
771
  }
@@ -676,8 +785,8 @@ ${errors.join("\n\n")}`
676
785
  ref.responses[itemStatus] = {
677
786
  description: itemDescription
678
787
  };
679
- if (item.properties?.headers !== void 0) {
680
- const headersSchema = resolveOpenAPIJsonSchemaRef(doc, item.properties.headers);
788
+ if (simplifiedItem.properties?.headers !== void 0) {
789
+ const headersSchema = simplifyComposedObjectJsonSchemasAndRefs(simplifiedItem.properties.headers, doc);
681
790
  if (!isObjectSchema(headersSchema)) {
682
791
  throw error;
683
792
  }
@@ -687,50 +796,52 @@ ${errors.join("\n\n")}`
687
796
  ref.responses[itemStatus].headers ??= {};
688
797
  ref.responses[itemStatus].headers[key] = {
689
798
  schema: toOpenAPISchema(headerSchema),
690
- required: item.required?.includes("headers") && headersSchema.required?.includes(key)
799
+ required: simplifiedItem.required?.includes("headers") && headersSchema.required?.includes(key)
691
800
  };
692
801
  }
693
802
  }
694
803
  }
695
- if (item.properties?.body !== void 0) {
804
+ if (simplifiedItem.properties?.body !== void 0) {
696
805
  ref.responses[itemStatus].content = toOpenAPIContent(
697
- applySchemaOptionality(item.required?.includes("body") ?? false, item.properties.body)
806
+ applySchemaOptionality(simplifiedItem.required?.includes("body") ?? false, simplifiedItem.properties.body)
698
807
  );
699
808
  }
700
809
  }
701
810
  }
702
- async #errorResponse(ref, def, baseSchemaConvertOptions, undefinedErrorSchema) {
811
+ async #errorResponse(ref, def, baseSchemaConvertOptions, undefinedErrorSchema, customErrorResponseBodySchema) {
703
812
  const errorMap = def.errorMap;
704
- const errors = {};
813
+ const errorResponsesByStatus = {};
705
814
  for (const code in errorMap) {
706
815
  const config = errorMap[code];
707
816
  if (!config) {
708
817
  continue;
709
818
  }
710
819
  const status = fallbackORPCErrorStatus(code, config.status);
711
- const message = fallbackORPCErrorMessage(code, config.message);
820
+ const defaultMessage = fallbackORPCErrorMessage(code, config.message);
821
+ errorResponsesByStatus[status] ??= { status, definedErrorDefinitions: [], errorSchemaVariants: [] };
712
822
  const [dataRequired, dataSchema] = await this.converter.convert(config.data, { ...baseSchemaConvertOptions, strategy: "output" });
713
- errors[status] ??= [];
714
- errors[status].push({
823
+ errorResponsesByStatus[status].definedErrorDefinitions.push([code, defaultMessage, dataRequired, dataSchema]);
824
+ errorResponsesByStatus[status].errorSchemaVariants.push({
715
825
  type: "object",
716
826
  properties: {
717
827
  defined: { const: true },
718
828
  code: { const: code },
719
829
  status: { const: status },
720
- message: { type: "string", default: message },
830
+ message: { type: "string", default: defaultMessage },
721
831
  data: dataSchema
722
832
  },
723
833
  required: dataRequired ? ["defined", "code", "status", "message", "data"] : ["defined", "code", "status", "message"]
724
834
  });
725
835
  }
726
836
  ref.responses ??= {};
727
- for (const status in errors) {
728
- const schemas = errors[status];
729
- ref.responses[status] = {
730
- description: status,
731
- content: toOpenAPIContent({
837
+ for (const statusString in errorResponsesByStatus) {
838
+ const errorResponse = errorResponsesByStatus[statusString];
839
+ const customBodySchema = value(customErrorResponseBodySchema, errorResponse.definedErrorDefinitions, errorResponse.status);
840
+ ref.responses[statusString] = {
841
+ description: statusString,
842
+ content: toOpenAPIContent(customBodySchema ?? {
732
843
  oneOf: [
733
- ...schemas,
844
+ ...errorResponse.errorSchemaVariants,
734
845
  undefinedErrorSchema
735
846
  ]
736
847
  })
@@ -739,4 +850,4 @@ ${errors.join("\n\n")}`
739
850
  }
740
851
  }
741
852
 
742
- export { CompositeSchemaConverter as C, LOGIC_KEYWORDS as L, OpenAPIGenerator as O, applyCustomOpenAPIOperation as a, toOpenAPIMethod as b, customOpenAPIOperation as c, toOpenAPIContent as d, toOpenAPIEventIteratorContent as e, toOpenAPIParameters as f, getCustomOpenAPIOperation as g, checkParamsSchema as h, toOpenAPISchema as i, isFileSchema as j, isObjectSchema as k, isAnySchema as l, filterSchemaBranches as m, applySchemaOptionality as n, expandUnionSchema as o, expandArrayableSchema as p, isPrimitiveSchema as q, resolveOpenAPIJsonSchemaRef as r, separateObjectSchema as s, toOpenAPIPath as t };
853
+ export { CompositeSchemaConverter as C, LOGIC_KEYWORDS as L, OpenAPIGenerator as O, applyCustomOpenAPIOperation as a, toOpenAPIMethod as b, customOpenAPIOperation as c, toOpenAPIContent as d, toOpenAPIEventIteratorContent as e, toOpenAPIParameters as f, getCustomOpenAPIOperation as g, checkParamsSchema as h, toOpenAPISchema as i, isFileSchema as j, isObjectSchema as k, isAnySchema as l, separateObjectSchema as m, filterSchemaBranches as n, applySchemaOptionality as o, expandUnionSchema as p, expandArrayableSchema as q, resolveOpenAPIJsonSchemaRef as r, simplifyComposedObjectJsonSchemasAndRefs as s, toOpenAPIPath as t, isPrimitiveSchema as u };
@@ -2,15 +2,17 @@ import { standardizeHTTPPath, StandardOpenAPIJsonSerializer, StandardBracketNota
2
2
  import { StandardHandler } from '@orpc/server/standard';
3
3
  import { isORPCErrorStatus } from '@orpc/client';
4
4
  import { fallbackContractConfig } from '@orpc/contract';
5
- import { isObject, stringifyJSON, value } from '@orpc/shared';
5
+ import { isObject, stringifyJSON, tryDecodeURIComponent, value } from '@orpc/shared';
6
6
  import { toHttpPath } from '@orpc/client/standard';
7
7
  import { traverseContractProcedures, isProcedure, getLazyMeta, unlazy, getRouter, createContractedProcedure } from '@orpc/server';
8
8
  import { createRouter, addRoute, findRoute } from 'rou3';
9
9
 
10
10
  class StandardOpenAPICodec {
11
- constructor(serializer) {
11
+ constructor(serializer, options = {}) {
12
12
  this.serializer = serializer;
13
+ this.customErrorResponseBodyEncoder = options.customErrorResponseBodyEncoder;
13
14
  }
15
+ customErrorResponseBodyEncoder;
14
16
  async decode(request, params, procedure) {
15
17
  const inputStructure = fallbackContractConfig("defaultInputStructure", procedure["~orpc"].route.inputStructure);
16
18
  if (inputStructure === "compact") {
@@ -73,10 +75,11 @@ class StandardOpenAPICodec {
73
75
  };
74
76
  }
75
77
  encodeError(error) {
78
+ const body = this.customErrorResponseBodyEncoder?.(error) ?? error.toJSON();
76
79
  return {
77
80
  status: error.status,
78
81
  headers: {},
79
- body: this.serializer.serialize(error.toJSON(), { outputFormat: "plain" })
82
+ body: this.serializer.serialize(body, { outputFormat: "plain" })
80
83
  };
81
84
  }
82
85
  #isDetailedOutput(output) {
@@ -97,7 +100,7 @@ function toRou3Pattern(path) {
97
100
  return standardizeHTTPPath(path).replace(/\/\{\+([^}]+)\}/g, "/**:$1").replace(/\/\{([^}]+)\}/g, "/:$1");
98
101
  }
99
102
  function decodeParams(params) {
100
- return Object.fromEntries(Object.entries(params).map(([key, value]) => [key, decodeURIComponent(value)]));
103
+ return Object.fromEntries(Object.entries(params).map(([key, value]) => [key, tryDecodeURIComponent(value)]));
101
104
  }
102
105
 
103
106
  class StandardOpenAPIMatcher {
@@ -179,7 +182,7 @@ class StandardOpenAPIHandler extends StandardHandler {
179
182
  const bracketNotationSerializer = new StandardBracketNotationSerializer(options);
180
183
  const serializer = new StandardOpenAPISerializer(jsonSerializer, bracketNotationSerializer);
181
184
  const matcher = new StandardOpenAPIMatcher(options);
182
- const codec = new StandardOpenAPICodec(serializer);
185
+ const codec = new StandardOpenAPICodec(serializer, options);
183
186
  super(router, matcher, codec, options);
184
187
  }
185
188
  }
@@ -0,0 +1,54 @@
1
+ import { StandardOpenAPISerializer, StandardOpenAPIJsonSerializerOptions, StandardBracketNotationSerializerOptions } from '@orpc/openapi-client/standard';
2
+ import { AnyProcedure, TraverseContractProcedureCallbackOptions, AnyRouter, Context, Router } from '@orpc/server';
3
+ import { StandardCodec, StandardParams, StandardMatcher, StandardMatchResult, StandardHandlerOptions, StandardHandler } from '@orpc/server/standard';
4
+ import { ORPCError, HTTPPath } from '@orpc/client';
5
+ import { StandardLazyRequest, StandardResponse } from '@orpc/standard-server';
6
+ import { Value } from '@orpc/shared';
7
+
8
+ interface StandardOpenAPICodecOptions {
9
+ /**
10
+ * Customize how an ORPC error is encoded into a response body.
11
+ * Use this if your API needs a different error output structure.
12
+ *
13
+ * @remarks
14
+ * - Return `null | undefined` to fallback to default behavior
15
+ *
16
+ * @default ((e) => e.toJSON())
17
+ */
18
+ customErrorResponseBodyEncoder?: (error: ORPCError<any, any>) => unknown;
19
+ }
20
+ declare class StandardOpenAPICodec implements StandardCodec {
21
+ #private;
22
+ private readonly serializer;
23
+ private readonly customErrorResponseBodyEncoder;
24
+ constructor(serializer: StandardOpenAPISerializer, options?: StandardOpenAPICodecOptions);
25
+ decode(request: StandardLazyRequest, params: StandardParams | undefined, procedure: AnyProcedure): Promise<unknown>;
26
+ encode(output: unknown, procedure: AnyProcedure): StandardResponse;
27
+ encodeError(error: ORPCError<any, any>): StandardResponse;
28
+ }
29
+
30
+ interface StandardOpenAPIMatcherOptions {
31
+ /**
32
+ * Filter procedures. Return `false` to exclude a procedure from matching.
33
+ *
34
+ * @default true
35
+ */
36
+ filter?: Value<boolean, [options: TraverseContractProcedureCallbackOptions]>;
37
+ }
38
+ declare class StandardOpenAPIMatcher implements StandardMatcher {
39
+ private readonly filter;
40
+ private readonly tree;
41
+ private pendingRouters;
42
+ constructor(options?: StandardOpenAPIMatcherOptions);
43
+ init(router: AnyRouter, path?: readonly string[]): void;
44
+ match(method: string, pathname: HTTPPath): Promise<StandardMatchResult>;
45
+ }
46
+
47
+ interface StandardOpenAPIHandlerOptions<T extends Context> extends StandardHandlerOptions<T>, StandardOpenAPIJsonSerializerOptions, StandardBracketNotationSerializerOptions, StandardOpenAPIMatcherOptions, StandardOpenAPICodecOptions {
48
+ }
49
+ declare class StandardOpenAPIHandler<T extends Context> extends StandardHandler<T> {
50
+ constructor(router: Router<any, T>, options: NoInfer<StandardOpenAPIHandlerOptions<T>>);
51
+ }
52
+
53
+ export { StandardOpenAPICodec as a, StandardOpenAPIHandler as c, StandardOpenAPIMatcher as e };
54
+ export type { StandardOpenAPICodecOptions as S, StandardOpenAPIHandlerOptions as b, StandardOpenAPIMatcherOptions as d };