hono-openapi 1.3.1 → 1.3.2

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.
package/dist/index.cjs CHANGED
@@ -6,6 +6,7 @@ var standardJson = require('@standard-community/standard-json');
6
6
  var standardOpenapi = require('@standard-community/standard-openapi');
7
7
 
8
8
  const uniqueSymbol = Symbol("openapi");
9
+ const VALIDATION_MARKER = "__HonoOpenAPIValidator__";
9
10
  const ALLOWED_METHODS = [
10
11
  "GET",
11
12
  "PUT",
@@ -58,7 +59,9 @@ const specsByPathContext = /* @__PURE__ */ new Map();
58
59
  function getPathContext(path) {
59
60
  const context = [];
60
61
  for (const [key, data] of specsByPathContext) {
61
- if (data && path.match(key)) {
62
+ if (!data) continue;
63
+ const prefix = key.endsWith("/*") ? key.slice(0, -2) : key;
64
+ if (path === prefix || path.startsWith(`${prefix}/`)) {
62
65
  context.push(data);
63
66
  }
64
67
  }
@@ -159,6 +162,8 @@ function removeExcludedPaths(paths, ctx) {
159
162
  for (const method of Object.keys(value)) {
160
163
  const schema = value[method];
161
164
  if (schema == null) continue;
165
+ const hasValidation = schema[VALIDATION_MARKER] === true;
166
+ delete schema[VALIDATION_MARKER];
162
167
  if (key.includes("{")) {
163
168
  schema.parameters = schema.parameters ? [...schema.parameters] : [];
164
169
  const pathParameters = key.split("/").filter(
@@ -198,6 +203,16 @@ function removeExcludedPaths(paths, ctx) {
198
203
  200: {}
199
204
  };
200
205
  }
206
+ if (hasValidation && ctx.options.defaultValidationErrorResponse !== false && !schema.responses["400"]) {
207
+ const errorResponse = ctx.options.defaultValidationErrorResponse;
208
+ if (typeof errorResponse === "object") {
209
+ schema.responses["400"] = structuredClone(errorResponse);
210
+ } else if (ctx.validationErrorResponse) {
211
+ schema.responses["400"] = structuredClone(
212
+ ctx.validationErrorResponse
213
+ );
214
+ }
215
+ }
201
216
  }
202
217
  const filteredValue = {};
203
218
  for (const method of Object.keys(value)) {
@@ -217,7 +232,27 @@ const DEFAULT_OPTIONS = {
217
232
  excludeStaticFile: true,
218
233
  exclude: [],
219
234
  excludeMethods: ["OPTIONS"],
220
- excludeTags: []
235
+ excludeTags: [],
236
+ defaultValidationErrorResponse: true
237
+ };
238
+ const DEFAULT_VALIDATION_ERROR = {
239
+ description: "Validation Error",
240
+ content: {
241
+ "application/json": {
242
+ schema: {
243
+ type: "object",
244
+ properties: {
245
+ success: { type: "boolean", enum: [false] },
246
+ error: {
247
+ type: "array",
248
+ items: {}
249
+ },
250
+ data: {}
251
+ },
252
+ required: ["success", "error", "data"]
253
+ }
254
+ }
255
+ }
221
256
  };
222
257
  function openAPIRouteHandler(hono, options) {
223
258
  let specs;
@@ -234,9 +269,14 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
234
269
  options: {
235
270
  ...DEFAULT_OPTIONS,
236
271
  ...options
237
- }
272
+ },
273
+ validationErrorResponse: DEFAULT_VALIDATION_ERROR
238
274
  };
239
275
  const _documentation = ctx.options.documentation ?? {};
276
+ const documentation = {
277
+ ..._documentation,
278
+ components: _documentation.components && { ..._documentation.components }
279
+ };
240
280
  clearSpecsContext();
241
281
  const paths = await generatePaths(hono, ctx);
242
282
  for (const path in paths) {
@@ -252,27 +292,30 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
252
292
  }
253
293
  }
254
294
  }
255
- const { components: resolvedDocComponents } = _documentation.components?.responses ? await resolveResponseSchemas(_documentation.components.responses) : { components: {} };
295
+ const resolvedDocumentation = documentation.components?.responses ? await resolveResponseSchemas(documentation.components.responses) : void 0;
296
+ if (resolvedDocumentation && documentation.components) {
297
+ documentation.components.responses = resolvedDocumentation.responses;
298
+ }
256
299
  const components = mergeComponentsObjects(
257
- _documentation.components,
258
- resolvedDocComponents,
300
+ documentation.components,
301
+ resolvedDocumentation?.components,
259
302
  ctx.components
260
303
  );
261
304
  return {
262
305
  openapi: "3.1.0",
263
- ..._documentation,
264
- tags: _documentation.tags?.filter(
306
+ ...documentation,
307
+ tags: documentation.tags?.filter(
265
308
  (tag) => !ctx.options.excludeTags?.includes(tag?.name)
266
309
  ),
267
310
  info: {
268
311
  title: "Hono Documentation",
269
312
  description: "Development documentation",
270
313
  version: "0.0.0",
271
- ..._documentation.info
314
+ ...documentation.info
272
315
  },
273
316
  paths: {
274
317
  ...removeExcludedPaths(paths, ctx),
275
- ..._documentation.paths
318
+ ...documentation.paths
276
319
  },
277
320
  components
278
321
  };
@@ -338,12 +381,27 @@ async function getSpec(middlewareHandler, defaultOptions) {
338
381
  let components = {};
339
382
  if (tmp.responses) {
340
383
  const resolved = await resolveResponseSchemas(tmp.responses);
384
+ tmp.responses = resolved.responses;
341
385
  components = resolved.components;
342
386
  }
387
+ if (tmp.requestBody && "content" in tmp.requestBody) {
388
+ const resolved = await resolveContentSchemas(tmp.requestBody.content);
389
+ tmp.requestBody = {
390
+ ...tmp.requestBody,
391
+ content: resolved.content
392
+ };
393
+ components = mergeComponentsObjects(components, resolved.components);
394
+ }
343
395
  return { schema: tmp, components };
344
396
  }
345
397
  const result = await middlewareHandler.toOpenAPISchema();
346
- const docs = { ...defaultOptions };
398
+ liftSchemaDefs(result);
399
+ const docs = {
400
+ ...defaultOptions,
401
+ // Mark this operation as validator-derived so a default 400 validation
402
+ // error response can be auto-injected later (see removeExcludedPaths).
403
+ [VALIDATION_MARKER]: true
404
+ };
347
405
  if (middlewareHandler.target === "form" || middlewareHandler.target === "json") {
348
406
  const media = middlewareHandler.options?.media ?? middlewareHandler.target === "json" ? "application/json" : "multipart/form-data";
349
407
  if (!docs.requestBody || !("content" in docs.requestBody) || !docs.requestBody.content) {
@@ -381,51 +439,113 @@ async function getSpec(middlewareHandler, defaultOptions) {
381
439
  });
382
440
  }
383
441
  } else {
384
- parameters = generateParameters(middlewareHandler.target, result.schema);
442
+ parameters = generateParameters(
443
+ middlewareHandler.target,
444
+ result.schema,
445
+ result.components?.schemas
446
+ );
385
447
  }
386
448
  docs.parameters = parameters;
387
449
  }
388
450
  return { schema: docs, components: result.components };
389
451
  }
390
- function generateParameters(target, schema) {
452
+ function generateParameters(target, schema, schemas) {
391
453
  const parameters = [];
392
- for (const [key, value] of Object.entries(schema.properties ?? {})) {
454
+ for (const [key, property] of collectParameterProperties(schema, schemas)) {
393
455
  const def = {
394
456
  in: target === "param" ? "path" : target,
395
457
  name: key,
396
- // @ts-expect-error
397
- schema: value
458
+ // @ts-expect-error ParameterObject uses the OpenAPI 3.0 schema type.
459
+ schema: property.schema
398
460
  };
399
- const isRequired = schema.required?.includes(key);
400
- if (isRequired) {
461
+ if (property.required) {
401
462
  def.required = true;
402
463
  }
403
464
  if (def.schema && "description" in def.schema && def.schema.description) {
404
465
  def.description = def.schema.description;
405
- def.schema.description = void 0;
466
+ def.schema = { ...def.schema, description: void 0 };
406
467
  }
407
468
  parameters.push(def);
408
469
  }
409
470
  return parameters;
410
471
  }
411
- async function resolveResponseSchemas(responses) {
412
- let components = {};
413
- for (const key of Object.keys(responses)) {
414
- const response = responses[key];
415
- if (!response || !("content" in response)) continue;
416
- for (const contentKey of Object.keys(response.content ?? {})) {
417
- const raw = response.content?.[contentKey];
418
- if (!raw) continue;
419
- if (raw.schema && "toOpenAPISchema" in raw.schema) {
420
- const result = await raw.schema.toOpenAPISchema();
421
- raw.schema = result.schema;
422
- if (result.components) {
423
- components = mergeComponentsObjects(components, result.components);
472
+ function collectParameterProperties(schema, schemas) {
473
+ const properties = /* @__PURE__ */ new Map();
474
+ const resolvingRefs = /* @__PURE__ */ new Set();
475
+ const collect = (current) => {
476
+ for (const [key, value] of Object.entries(current.properties ?? {})) {
477
+ const existing = properties.get(key);
478
+ properties.set(key, {
479
+ schema: existing ? { allOf: [existing.schema, value] } : value,
480
+ required: Boolean(current.required?.includes(key)) || existing?.required
481
+ });
482
+ }
483
+ for (const member of current.allOf ?? []) {
484
+ if ("$ref" in member) {
485
+ const prefix = "#/components/schemas/";
486
+ if (!member.$ref.startsWith(prefix)) continue;
487
+ const name = member.$ref.slice(prefix.length).replaceAll("~1", "/").replaceAll("~0", "~");
488
+ const referencedSchema = schemas?.[name];
489
+ if (referencedSchema && !resolvingRefs.has(name)) {
490
+ resolvingRefs.add(name);
491
+ collect(referencedSchema);
492
+ resolvingRefs.delete(name);
424
493
  }
494
+ } else {
495
+ collect(member);
496
+ }
497
+ }
498
+ };
499
+ collect(schema);
500
+ return properties;
501
+ }
502
+ async function resolveContentSchemas(content) {
503
+ let components = {};
504
+ const resolvedContent = {};
505
+ for (const [contentKey, raw] of Object.entries(content)) {
506
+ if (!raw) continue;
507
+ if (raw.schema && "toOpenAPISchema" in raw.schema) {
508
+ const result = await raw.schema.toOpenAPISchema();
509
+ liftSchemaDefs(result);
510
+ resolvedContent[contentKey] = {
511
+ ...raw,
512
+ schema: result.schema
513
+ };
514
+ if (result.components) {
515
+ components = mergeComponentsObjects(components, result.components);
425
516
  }
517
+ } else {
518
+ resolvedContent[contentKey] = raw;
519
+ }
520
+ }
521
+ return { content: resolvedContent, components };
522
+ }
523
+ async function resolveResponseSchemas(responses) {
524
+ let components = {};
525
+ const resolvedResponses = {};
526
+ for (const [key, response] of Object.entries(responses)) {
527
+ if (!response || !("content" in response) || !response.content) {
528
+ resolvedResponses[key] = response;
529
+ continue;
426
530
  }
531
+ const resolved = await resolveContentSchemas(response.content);
532
+ resolvedResponses[key] = {
533
+ ...response,
534
+ content: resolved.content
535
+ };
536
+ components = mergeComponentsObjects(components, resolved.components);
427
537
  }
428
- return { responses, components };
538
+ return { responses: resolvedResponses, components };
539
+ }
540
+ function liftSchemaDefs(result) {
541
+ const defs = result.schema.$defs;
542
+ if (!defs || typeof defs !== "object") return;
543
+ result.components ??= {};
544
+ result.components.schemas = {
545
+ ...defs,
546
+ ...result.components.schemas
547
+ };
548
+ delete result.schema.$defs;
429
549
  }
430
550
  function mergeComponentsObjects(...components) {
431
551
  return components.reduce(
@@ -571,6 +691,7 @@ function describeResponse(handler, responses, options) {
571
691
  }
572
692
 
573
693
  exports.ALLOWED_METHODS = ALLOWED_METHODS;
694
+ exports.VALIDATION_MARKER = VALIDATION_MARKER;
574
695
  exports.clearSpecsContext = clearSpecsContext;
575
696
  exports.describeResponse = describeResponse;
576
697
  exports.describeRoute = describeRoute;
package/dist/index.d.cts CHANGED
@@ -5,68 +5,12 @@ import { TypedResponse, RouterRoute, ValidationTargets as ValidationTargets$1, B
5
5
  import { loadVendor as loadVendor$2, ToOpenAPISchemaContext } from '@standard-community/standard-openapi';
6
6
  import { Hook } from '@hono/standard-validator';
7
7
  import { loadVendor as loadVendor$1 } from '@standard-community/standard-json';
8
+ import { StandardSchemaV1 } from '@standard-schema/spec';
8
9
  import { StatusCode } from 'hono/utils/http-status';
9
10
  import { JSONParsed } from 'hono/utils/types';
10
11
  import { InferInput } from 'hono/validator';
11
12
  import { JSONSchema7 } from 'json-schema';
12
13
 
13
- /** The Standard Schema interface. */
14
- interface StandardSchemaV1<Input = unknown, Output = Input> {
15
- /** The Standard Schema properties. */
16
- readonly "~standard": StandardSchemaV1.Props<Input, Output>;
17
- }
18
- declare namespace StandardSchemaV1 {
19
- /** The Standard Schema properties interface. */
20
- export interface Props<Input = unknown, Output = Input> {
21
- /** The version number of the standard. */
22
- readonly version: 1;
23
- /** The vendor name of the schema library. */
24
- readonly vendor: string;
25
- /** Validates unknown input values. */
26
- readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;
27
- /** Inferred types associated with the schema. */
28
- readonly types?: Types<Input, Output> | undefined;
29
- }
30
- /** The result interface of the validate function. */
31
- export type Result<Output> = SuccessResult<Output> | FailureResult;
32
- /** The result interface if validation succeeds. */
33
- export interface SuccessResult<Output> {
34
- /** The typed output value. */
35
- readonly value: Output;
36
- /** The non-existent issues. */
37
- readonly issues?: undefined;
38
- }
39
- /** The result interface if validation fails. */
40
- export interface FailureResult {
41
- /** The issues of failed validation. */
42
- readonly issues: ReadonlyArray<Issue>;
43
- }
44
- /** The issue interface of the failure output. */
45
- export interface Issue {
46
- /** The error message of the issue. */
47
- readonly message: string;
48
- /** The path of the issue, if any. */
49
- readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
50
- }
51
- /** The path segment interface of the issue. */
52
- export interface PathSegment {
53
- /** The key representing a path segment. */
54
- readonly key: PropertyKey;
55
- }
56
- /** The Standard Schema types interface. */
57
- export interface Types<Input = unknown, Output = Input> {
58
- /** The input type of the schema. */
59
- readonly input: Input;
60
- /** The output type of the schema. */
61
- readonly output: Output;
62
- }
63
- /** Infers the input type of a Standard Schema. */
64
- export type InferInput<Schema extends StandardSchemaV1> = NonNullable<Schema["~standard"]["types"]>["input"];
65
- /** Infers the output type of a Standard Schema. */
66
- export type InferOutput<Schema extends StandardSchemaV1> = NonNullable<Schema["~standard"]["types"]>["output"];
67
- export { };
68
- }
69
-
70
14
  declare function loadVendor(vendor: string, fn: {
71
15
  toJSONSchema?: Parameters<typeof loadVendor$1>[1];
72
16
  toOpenAPISchema?: Parameters<typeof loadVendor$2>[1];
@@ -129,6 +73,14 @@ declare function describeResponse<E extends Env, P extends string, I extends Inp
129
73
  * The unique symbol for the middlewares, which makes it easier to identify them. Not meant to be used directly, unless you're creating a custom middleware.
130
74
  */
131
75
  declare const uniqueSymbol: unique symbol;
76
+ /**
77
+ * Internal marker key set on an operation's spec when it is produced by a
78
+ * `validator()` middleware. It is used to decide whether to auto-inject the
79
+ * default 400 validation error response, and is stripped from the operation
80
+ * before the spec is emitted. Must be an enumerable string key so it survives
81
+ * the `Object.entries`-based merge in `mergeSpecs`.
82
+ */
83
+ declare const VALIDATION_MARKER = "__HonoOpenAPIValidator__";
132
84
  declare const ALLOWED_METHODS: readonly ["GET", "PUT", "POST", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE"];
133
85
  type AllowedMethods = (typeof ALLOWED_METHODS)[number];
134
86
  declare function clearSpecsContext(): void;
@@ -155,13 +107,17 @@ type HandlerUniqueProperty = (ResolverReturnType & {
155
107
  type MediaTypeObjectWithResolver = Omit<OpenAPIV3_1.MediaTypeObject, "schema"> & {
156
108
  schema?: OpenAPIV3_1.ReferenceObject | OpenAPIV3_1.SchemaObject | ResolverReturnType;
157
109
  };
110
+ type ContentWithResolver = {
111
+ [media: string]: MediaTypeObjectWithResolver;
112
+ };
158
113
  /**
159
114
  * A response object that accepts resolver() output in schema positions.
160
115
  */
161
116
  type ResponseObjectWithResolver = (Omit<OpenAPIV3_1.ResponseObject, "content"> & {
162
- content?: {
163
- [media: string]: MediaTypeObjectWithResolver;
164
- };
117
+ content?: ContentWithResolver;
118
+ }) | OpenAPIV3_1.ReferenceObject;
119
+ type RequestBodyObjectWithResolver = (Omit<OpenAPIV3_1.RequestBodyObject, "content"> & {
120
+ content: ContentWithResolver;
165
121
  }) | OpenAPIV3_1.ReferenceObject;
166
122
  /**
167
123
  * A responses map that accepts resolver() output in schema positions.
@@ -214,10 +170,23 @@ type GenerateSpecOptions = {
214
170
  * Default options for `describeRoute` method
215
171
  */
216
172
  defaultOptions: Partial<Record<AllowedMethods | "ALL", DescribeRouteOptions>>;
173
+ /**
174
+ * Automatically include a 400 validation error response for routes
175
+ * that use `validator()`. Set to `false` to disable, `true` to use
176
+ * the built-in schema, or provide a custom `ResponseObject`.
177
+ *
178
+ * @default true
179
+ */
180
+ defaultValidationErrorResponse: boolean | OpenAPIV3_1.ResponseObject;
217
181
  };
218
182
  type OperationId = string | ((route: RouterRoute) => string);
219
- type DescribeRouteOptions = Omit<OpenAPIV3_1.OperationObject, "responses" | "operationId"> & {
183
+ type DescribeRouteOptions = Omit<OpenAPIV3_1.OperationObject, "requestBody" | "responses" | "operationId"> & {
220
184
  operationId?: OperationId;
185
+ /**
186
+ * Request body metadata. `resolver()` values are converted to OpenAPI
187
+ * schemas while generating the document.
188
+ */
189
+ requestBody?: RequestBodyObjectWithResolver;
221
190
  /**
222
191
  * Pass `true` to hide route from OpenAPI/swagger document
223
192
  */
@@ -238,11 +207,12 @@ type RegisterSchemaPathOptions = {
238
207
  };
239
208
  paths: Partial<OpenAPIV3_1.PathsObject>;
240
209
  };
241
- type HaveDefaultValues = "documentation" | "excludeStaticFile" | "exclude" | "excludeMethods" | "excludeTags";
210
+ type HaveDefaultValues = "documentation" | "excludeStaticFile" | "exclude" | "excludeMethods" | "excludeTags" | "defaultValidationErrorResponse";
242
211
  type SanitizedGenerateSpecOptions = Pick<GenerateSpecOptions, HaveDefaultValues> & Omit<Partial<GenerateSpecOptions>, HaveDefaultValues>;
243
212
  type SpecContext = {
244
213
  components: OpenAPIV3_1.ComponentsObject;
245
214
  options: SanitizedGenerateSpecOptions;
215
+ validationErrorResponse?: OpenAPIV3_1.ResponseObject;
246
216
  };
247
217
 
248
218
  /**
@@ -295,4 +265,4 @@ declare function generateSpecs<E extends Env = BlankEnv, P extends string = stri
295
265
  jsonSchemaDialect?: string;
296
266
  }>;
297
267
 
298
- export { ALLOWED_METHODS, type AllowedMethods, type DescribeRouteOptions, type GenerateSpecOptions, type HandlerUniqueProperty, type PromiseOr, type RegisterSchemaPathOptions, type ResolverReturnType, type ResponsesWithResolver, type SpecContext, clearSpecsContext, describeResponse, describeRoute, generateSpecs, loadVendor, openAPIRouteHandler, registerSchemaPath, removeExcludedPaths, resolver, uniqueSymbol, validator };
268
+ export { ALLOWED_METHODS, type AllowedMethods, type ContentWithResolver, type DescribeRouteOptions, type GenerateSpecOptions, type HandlerUniqueProperty, type PromiseOr, type RegisterSchemaPathOptions, type ResolverReturnType, type ResponsesWithResolver, type SpecContext, VALIDATION_MARKER, clearSpecsContext, describeResponse, describeRoute, generateSpecs, loadVendor, openAPIRouteHandler, registerSchemaPath, removeExcludedPaths, resolver, uniqueSymbol, validator };
package/dist/index.d.ts CHANGED
@@ -5,68 +5,12 @@ import { TypedResponse, RouterRoute, ValidationTargets as ValidationTargets$1, B
5
5
  import { loadVendor as loadVendor$2, ToOpenAPISchemaContext } from '@standard-community/standard-openapi';
6
6
  import { Hook } from '@hono/standard-validator';
7
7
  import { loadVendor as loadVendor$1 } from '@standard-community/standard-json';
8
+ import { StandardSchemaV1 } from '@standard-schema/spec';
8
9
  import { StatusCode } from 'hono/utils/http-status';
9
10
  import { JSONParsed } from 'hono/utils/types';
10
11
  import { InferInput } from 'hono/validator';
11
12
  import { JSONSchema7 } from 'json-schema';
12
13
 
13
- /** The Standard Schema interface. */
14
- interface StandardSchemaV1<Input = unknown, Output = Input> {
15
- /** The Standard Schema properties. */
16
- readonly "~standard": StandardSchemaV1.Props<Input, Output>;
17
- }
18
- declare namespace StandardSchemaV1 {
19
- /** The Standard Schema properties interface. */
20
- export interface Props<Input = unknown, Output = Input> {
21
- /** The version number of the standard. */
22
- readonly version: 1;
23
- /** The vendor name of the schema library. */
24
- readonly vendor: string;
25
- /** Validates unknown input values. */
26
- readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;
27
- /** Inferred types associated with the schema. */
28
- readonly types?: Types<Input, Output> | undefined;
29
- }
30
- /** The result interface of the validate function. */
31
- export type Result<Output> = SuccessResult<Output> | FailureResult;
32
- /** The result interface if validation succeeds. */
33
- export interface SuccessResult<Output> {
34
- /** The typed output value. */
35
- readonly value: Output;
36
- /** The non-existent issues. */
37
- readonly issues?: undefined;
38
- }
39
- /** The result interface if validation fails. */
40
- export interface FailureResult {
41
- /** The issues of failed validation. */
42
- readonly issues: ReadonlyArray<Issue>;
43
- }
44
- /** The issue interface of the failure output. */
45
- export interface Issue {
46
- /** The error message of the issue. */
47
- readonly message: string;
48
- /** The path of the issue, if any. */
49
- readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
50
- }
51
- /** The path segment interface of the issue. */
52
- export interface PathSegment {
53
- /** The key representing a path segment. */
54
- readonly key: PropertyKey;
55
- }
56
- /** The Standard Schema types interface. */
57
- export interface Types<Input = unknown, Output = Input> {
58
- /** The input type of the schema. */
59
- readonly input: Input;
60
- /** The output type of the schema. */
61
- readonly output: Output;
62
- }
63
- /** Infers the input type of a Standard Schema. */
64
- export type InferInput<Schema extends StandardSchemaV1> = NonNullable<Schema["~standard"]["types"]>["input"];
65
- /** Infers the output type of a Standard Schema. */
66
- export type InferOutput<Schema extends StandardSchemaV1> = NonNullable<Schema["~standard"]["types"]>["output"];
67
- export { };
68
- }
69
-
70
14
  declare function loadVendor(vendor: string, fn: {
71
15
  toJSONSchema?: Parameters<typeof loadVendor$1>[1];
72
16
  toOpenAPISchema?: Parameters<typeof loadVendor$2>[1];
@@ -129,6 +73,14 @@ declare function describeResponse<E extends Env, P extends string, I extends Inp
129
73
  * The unique symbol for the middlewares, which makes it easier to identify them. Not meant to be used directly, unless you're creating a custom middleware.
130
74
  */
131
75
  declare const uniqueSymbol: unique symbol;
76
+ /**
77
+ * Internal marker key set on an operation's spec when it is produced by a
78
+ * `validator()` middleware. It is used to decide whether to auto-inject the
79
+ * default 400 validation error response, and is stripped from the operation
80
+ * before the spec is emitted. Must be an enumerable string key so it survives
81
+ * the `Object.entries`-based merge in `mergeSpecs`.
82
+ */
83
+ declare const VALIDATION_MARKER = "__HonoOpenAPIValidator__";
132
84
  declare const ALLOWED_METHODS: readonly ["GET", "PUT", "POST", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE"];
133
85
  type AllowedMethods = (typeof ALLOWED_METHODS)[number];
134
86
  declare function clearSpecsContext(): void;
@@ -155,13 +107,17 @@ type HandlerUniqueProperty = (ResolverReturnType & {
155
107
  type MediaTypeObjectWithResolver = Omit<OpenAPIV3_1.MediaTypeObject, "schema"> & {
156
108
  schema?: OpenAPIV3_1.ReferenceObject | OpenAPIV3_1.SchemaObject | ResolverReturnType;
157
109
  };
110
+ type ContentWithResolver = {
111
+ [media: string]: MediaTypeObjectWithResolver;
112
+ };
158
113
  /**
159
114
  * A response object that accepts resolver() output in schema positions.
160
115
  */
161
116
  type ResponseObjectWithResolver = (Omit<OpenAPIV3_1.ResponseObject, "content"> & {
162
- content?: {
163
- [media: string]: MediaTypeObjectWithResolver;
164
- };
117
+ content?: ContentWithResolver;
118
+ }) | OpenAPIV3_1.ReferenceObject;
119
+ type RequestBodyObjectWithResolver = (Omit<OpenAPIV3_1.RequestBodyObject, "content"> & {
120
+ content: ContentWithResolver;
165
121
  }) | OpenAPIV3_1.ReferenceObject;
166
122
  /**
167
123
  * A responses map that accepts resolver() output in schema positions.
@@ -214,10 +170,23 @@ type GenerateSpecOptions = {
214
170
  * Default options for `describeRoute` method
215
171
  */
216
172
  defaultOptions: Partial<Record<AllowedMethods | "ALL", DescribeRouteOptions>>;
173
+ /**
174
+ * Automatically include a 400 validation error response for routes
175
+ * that use `validator()`. Set to `false` to disable, `true` to use
176
+ * the built-in schema, or provide a custom `ResponseObject`.
177
+ *
178
+ * @default true
179
+ */
180
+ defaultValidationErrorResponse: boolean | OpenAPIV3_1.ResponseObject;
217
181
  };
218
182
  type OperationId = string | ((route: RouterRoute) => string);
219
- type DescribeRouteOptions = Omit<OpenAPIV3_1.OperationObject, "responses" | "operationId"> & {
183
+ type DescribeRouteOptions = Omit<OpenAPIV3_1.OperationObject, "requestBody" | "responses" | "operationId"> & {
220
184
  operationId?: OperationId;
185
+ /**
186
+ * Request body metadata. `resolver()` values are converted to OpenAPI
187
+ * schemas while generating the document.
188
+ */
189
+ requestBody?: RequestBodyObjectWithResolver;
221
190
  /**
222
191
  * Pass `true` to hide route from OpenAPI/swagger document
223
192
  */
@@ -238,11 +207,12 @@ type RegisterSchemaPathOptions = {
238
207
  };
239
208
  paths: Partial<OpenAPIV3_1.PathsObject>;
240
209
  };
241
- type HaveDefaultValues = "documentation" | "excludeStaticFile" | "exclude" | "excludeMethods" | "excludeTags";
210
+ type HaveDefaultValues = "documentation" | "excludeStaticFile" | "exclude" | "excludeMethods" | "excludeTags" | "defaultValidationErrorResponse";
242
211
  type SanitizedGenerateSpecOptions = Pick<GenerateSpecOptions, HaveDefaultValues> & Omit<Partial<GenerateSpecOptions>, HaveDefaultValues>;
243
212
  type SpecContext = {
244
213
  components: OpenAPIV3_1.ComponentsObject;
245
214
  options: SanitizedGenerateSpecOptions;
215
+ validationErrorResponse?: OpenAPIV3_1.ResponseObject;
246
216
  };
247
217
 
248
218
  /**
@@ -295,4 +265,4 @@ declare function generateSpecs<E extends Env = BlankEnv, P extends string = stri
295
265
  jsonSchemaDialect?: string;
296
266
  }>;
297
267
 
298
- export { ALLOWED_METHODS, type AllowedMethods, type DescribeRouteOptions, type GenerateSpecOptions, type HandlerUniqueProperty, type PromiseOr, type RegisterSchemaPathOptions, type ResolverReturnType, type ResponsesWithResolver, type SpecContext, clearSpecsContext, describeResponse, describeRoute, generateSpecs, loadVendor, openAPIRouteHandler, registerSchemaPath, removeExcludedPaths, resolver, uniqueSymbol, validator };
268
+ export { ALLOWED_METHODS, type AllowedMethods, type ContentWithResolver, type DescribeRouteOptions, type GenerateSpecOptions, type HandlerUniqueProperty, type PromiseOr, type RegisterSchemaPathOptions, type ResolverReturnType, type ResponsesWithResolver, type SpecContext, VALIDATION_MARKER, clearSpecsContext, describeResponse, describeRoute, generateSpecs, loadVendor, openAPIRouteHandler, registerSchemaPath, removeExcludedPaths, resolver, uniqueSymbol, validator };
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import { loadVendor as loadVendor$1, toJsonSchema } from '@standard-community/st
4
4
  import { loadVendor as loadVendor$2, toOpenAPISchema } from '@standard-community/standard-openapi';
5
5
 
6
6
  const uniqueSymbol = Symbol("openapi");
7
+ const VALIDATION_MARKER = "__HonoOpenAPIValidator__";
7
8
  const ALLOWED_METHODS = [
8
9
  "GET",
9
10
  "PUT",
@@ -56,7 +57,9 @@ const specsByPathContext = /* @__PURE__ */ new Map();
56
57
  function getPathContext(path) {
57
58
  const context = [];
58
59
  for (const [key, data] of specsByPathContext) {
59
- if (data && path.match(key)) {
60
+ if (!data) continue;
61
+ const prefix = key.endsWith("/*") ? key.slice(0, -2) : key;
62
+ if (path === prefix || path.startsWith(`${prefix}/`)) {
60
63
  context.push(data);
61
64
  }
62
65
  }
@@ -157,6 +160,8 @@ function removeExcludedPaths(paths, ctx) {
157
160
  for (const method of Object.keys(value)) {
158
161
  const schema = value[method];
159
162
  if (schema == null) continue;
163
+ const hasValidation = schema[VALIDATION_MARKER] === true;
164
+ delete schema[VALIDATION_MARKER];
160
165
  if (key.includes("{")) {
161
166
  schema.parameters = schema.parameters ? [...schema.parameters] : [];
162
167
  const pathParameters = key.split("/").filter(
@@ -196,6 +201,16 @@ function removeExcludedPaths(paths, ctx) {
196
201
  200: {}
197
202
  };
198
203
  }
204
+ if (hasValidation && ctx.options.defaultValidationErrorResponse !== false && !schema.responses["400"]) {
205
+ const errorResponse = ctx.options.defaultValidationErrorResponse;
206
+ if (typeof errorResponse === "object") {
207
+ schema.responses["400"] = structuredClone(errorResponse);
208
+ } else if (ctx.validationErrorResponse) {
209
+ schema.responses["400"] = structuredClone(
210
+ ctx.validationErrorResponse
211
+ );
212
+ }
213
+ }
199
214
  }
200
215
  const filteredValue = {};
201
216
  for (const method of Object.keys(value)) {
@@ -215,7 +230,27 @@ const DEFAULT_OPTIONS = {
215
230
  excludeStaticFile: true,
216
231
  exclude: [],
217
232
  excludeMethods: ["OPTIONS"],
218
- excludeTags: []
233
+ excludeTags: [],
234
+ defaultValidationErrorResponse: true
235
+ };
236
+ const DEFAULT_VALIDATION_ERROR = {
237
+ description: "Validation Error",
238
+ content: {
239
+ "application/json": {
240
+ schema: {
241
+ type: "object",
242
+ properties: {
243
+ success: { type: "boolean", enum: [false] },
244
+ error: {
245
+ type: "array",
246
+ items: {}
247
+ },
248
+ data: {}
249
+ },
250
+ required: ["success", "error", "data"]
251
+ }
252
+ }
253
+ }
219
254
  };
220
255
  function openAPIRouteHandler(hono, options) {
221
256
  let specs;
@@ -232,9 +267,14 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
232
267
  options: {
233
268
  ...DEFAULT_OPTIONS,
234
269
  ...options
235
- }
270
+ },
271
+ validationErrorResponse: DEFAULT_VALIDATION_ERROR
236
272
  };
237
273
  const _documentation = ctx.options.documentation ?? {};
274
+ const documentation = {
275
+ ..._documentation,
276
+ components: _documentation.components && { ..._documentation.components }
277
+ };
238
278
  clearSpecsContext();
239
279
  const paths = await generatePaths(hono, ctx);
240
280
  for (const path in paths) {
@@ -250,27 +290,30 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
250
290
  }
251
291
  }
252
292
  }
253
- const { components: resolvedDocComponents } = _documentation.components?.responses ? await resolveResponseSchemas(_documentation.components.responses) : { components: {} };
293
+ const resolvedDocumentation = documentation.components?.responses ? await resolveResponseSchemas(documentation.components.responses) : void 0;
294
+ if (resolvedDocumentation && documentation.components) {
295
+ documentation.components.responses = resolvedDocumentation.responses;
296
+ }
254
297
  const components = mergeComponentsObjects(
255
- _documentation.components,
256
- resolvedDocComponents,
298
+ documentation.components,
299
+ resolvedDocumentation?.components,
257
300
  ctx.components
258
301
  );
259
302
  return {
260
303
  openapi: "3.1.0",
261
- ..._documentation,
262
- tags: _documentation.tags?.filter(
304
+ ...documentation,
305
+ tags: documentation.tags?.filter(
263
306
  (tag) => !ctx.options.excludeTags?.includes(tag?.name)
264
307
  ),
265
308
  info: {
266
309
  title: "Hono Documentation",
267
310
  description: "Development documentation",
268
311
  version: "0.0.0",
269
- ..._documentation.info
312
+ ...documentation.info
270
313
  },
271
314
  paths: {
272
315
  ...removeExcludedPaths(paths, ctx),
273
- ..._documentation.paths
316
+ ...documentation.paths
274
317
  },
275
318
  components
276
319
  };
@@ -336,12 +379,27 @@ async function getSpec(middlewareHandler, defaultOptions) {
336
379
  let components = {};
337
380
  if (tmp.responses) {
338
381
  const resolved = await resolveResponseSchemas(tmp.responses);
382
+ tmp.responses = resolved.responses;
339
383
  components = resolved.components;
340
384
  }
385
+ if (tmp.requestBody && "content" in tmp.requestBody) {
386
+ const resolved = await resolveContentSchemas(tmp.requestBody.content);
387
+ tmp.requestBody = {
388
+ ...tmp.requestBody,
389
+ content: resolved.content
390
+ };
391
+ components = mergeComponentsObjects(components, resolved.components);
392
+ }
341
393
  return { schema: tmp, components };
342
394
  }
343
395
  const result = await middlewareHandler.toOpenAPISchema();
344
- const docs = { ...defaultOptions };
396
+ liftSchemaDefs(result);
397
+ const docs = {
398
+ ...defaultOptions,
399
+ // Mark this operation as validator-derived so a default 400 validation
400
+ // error response can be auto-injected later (see removeExcludedPaths).
401
+ [VALIDATION_MARKER]: true
402
+ };
345
403
  if (middlewareHandler.target === "form" || middlewareHandler.target === "json") {
346
404
  const media = middlewareHandler.options?.media ?? middlewareHandler.target === "json" ? "application/json" : "multipart/form-data";
347
405
  if (!docs.requestBody || !("content" in docs.requestBody) || !docs.requestBody.content) {
@@ -379,51 +437,113 @@ async function getSpec(middlewareHandler, defaultOptions) {
379
437
  });
380
438
  }
381
439
  } else {
382
- parameters = generateParameters(middlewareHandler.target, result.schema);
440
+ parameters = generateParameters(
441
+ middlewareHandler.target,
442
+ result.schema,
443
+ result.components?.schemas
444
+ );
383
445
  }
384
446
  docs.parameters = parameters;
385
447
  }
386
448
  return { schema: docs, components: result.components };
387
449
  }
388
- function generateParameters(target, schema) {
450
+ function generateParameters(target, schema, schemas) {
389
451
  const parameters = [];
390
- for (const [key, value] of Object.entries(schema.properties ?? {})) {
452
+ for (const [key, property] of collectParameterProperties(schema, schemas)) {
391
453
  const def = {
392
454
  in: target === "param" ? "path" : target,
393
455
  name: key,
394
- // @ts-expect-error
395
- schema: value
456
+ // @ts-expect-error ParameterObject uses the OpenAPI 3.0 schema type.
457
+ schema: property.schema
396
458
  };
397
- const isRequired = schema.required?.includes(key);
398
- if (isRequired) {
459
+ if (property.required) {
399
460
  def.required = true;
400
461
  }
401
462
  if (def.schema && "description" in def.schema && def.schema.description) {
402
463
  def.description = def.schema.description;
403
- def.schema.description = void 0;
464
+ def.schema = { ...def.schema, description: void 0 };
404
465
  }
405
466
  parameters.push(def);
406
467
  }
407
468
  return parameters;
408
469
  }
409
- async function resolveResponseSchemas(responses) {
410
- let components = {};
411
- for (const key of Object.keys(responses)) {
412
- const response = responses[key];
413
- if (!response || !("content" in response)) continue;
414
- for (const contentKey of Object.keys(response.content ?? {})) {
415
- const raw = response.content?.[contentKey];
416
- if (!raw) continue;
417
- if (raw.schema && "toOpenAPISchema" in raw.schema) {
418
- const result = await raw.schema.toOpenAPISchema();
419
- raw.schema = result.schema;
420
- if (result.components) {
421
- components = mergeComponentsObjects(components, result.components);
470
+ function collectParameterProperties(schema, schemas) {
471
+ const properties = /* @__PURE__ */ new Map();
472
+ const resolvingRefs = /* @__PURE__ */ new Set();
473
+ const collect = (current) => {
474
+ for (const [key, value] of Object.entries(current.properties ?? {})) {
475
+ const existing = properties.get(key);
476
+ properties.set(key, {
477
+ schema: existing ? { allOf: [existing.schema, value] } : value,
478
+ required: Boolean(current.required?.includes(key)) || existing?.required
479
+ });
480
+ }
481
+ for (const member of current.allOf ?? []) {
482
+ if ("$ref" in member) {
483
+ const prefix = "#/components/schemas/";
484
+ if (!member.$ref.startsWith(prefix)) continue;
485
+ const name = member.$ref.slice(prefix.length).replaceAll("~1", "/").replaceAll("~0", "~");
486
+ const referencedSchema = schemas?.[name];
487
+ if (referencedSchema && !resolvingRefs.has(name)) {
488
+ resolvingRefs.add(name);
489
+ collect(referencedSchema);
490
+ resolvingRefs.delete(name);
422
491
  }
492
+ } else {
493
+ collect(member);
494
+ }
495
+ }
496
+ };
497
+ collect(schema);
498
+ return properties;
499
+ }
500
+ async function resolveContentSchemas(content) {
501
+ let components = {};
502
+ const resolvedContent = {};
503
+ for (const [contentKey, raw] of Object.entries(content)) {
504
+ if (!raw) continue;
505
+ if (raw.schema && "toOpenAPISchema" in raw.schema) {
506
+ const result = await raw.schema.toOpenAPISchema();
507
+ liftSchemaDefs(result);
508
+ resolvedContent[contentKey] = {
509
+ ...raw,
510
+ schema: result.schema
511
+ };
512
+ if (result.components) {
513
+ components = mergeComponentsObjects(components, result.components);
423
514
  }
515
+ } else {
516
+ resolvedContent[contentKey] = raw;
517
+ }
518
+ }
519
+ return { content: resolvedContent, components };
520
+ }
521
+ async function resolveResponseSchemas(responses) {
522
+ let components = {};
523
+ const resolvedResponses = {};
524
+ for (const [key, response] of Object.entries(responses)) {
525
+ if (!response || !("content" in response) || !response.content) {
526
+ resolvedResponses[key] = response;
527
+ continue;
424
528
  }
529
+ const resolved = await resolveContentSchemas(response.content);
530
+ resolvedResponses[key] = {
531
+ ...response,
532
+ content: resolved.content
533
+ };
534
+ components = mergeComponentsObjects(components, resolved.components);
425
535
  }
426
- return { responses, components };
536
+ return { responses: resolvedResponses, components };
537
+ }
538
+ function liftSchemaDefs(result) {
539
+ const defs = result.schema.$defs;
540
+ if (!defs || typeof defs !== "object") return;
541
+ result.components ??= {};
542
+ result.components.schemas = {
543
+ ...defs,
544
+ ...result.components.schemas
545
+ };
546
+ delete result.schema.$defs;
427
547
  }
428
548
  function mergeComponentsObjects(...components) {
429
549
  return components.reduce(
@@ -568,4 +688,4 @@ function describeResponse(handler, responses, options) {
568
688
  });
569
689
  }
570
690
 
571
- export { ALLOWED_METHODS, clearSpecsContext, describeResponse, describeRoute, generateSpecs, loadVendor, openAPIRouteHandler, registerSchemaPath, removeExcludedPaths, resolver, uniqueSymbol, validator };
691
+ export { ALLOWED_METHODS, VALIDATION_MARKER, clearSpecsContext, describeResponse, describeRoute, generateSpecs, loadVendor, openAPIRouteHandler, registerSchemaPath, removeExcludedPaths, resolver, uniqueSymbol, validator };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hono-openapi",
3
3
  "description": "OpenAPI schema generator for Hono",
4
- "version": "1.3.1",
4
+ "version": "1.3.2",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
7
7
  "module": "dist/index.js",
@@ -40,28 +40,22 @@
40
40
  "default": "./dist/index.cjs"
41
41
  }
42
42
  },
43
- "peerDependencies": {
44
- "@hono/standard-validator": "^0.2.0",
43
+ "dependencies": {
44
+ "@hono/standard-validator": "^0.4.0",
45
45
  "@standard-community/standard-json": "^0.3.5",
46
46
  "@standard-community/standard-openapi": "^0.2.9",
47
+ "@standard-schema/spec": "^1.0.0",
47
48
  "@types/json-schema": "^7.0.15",
48
- "hono": "^4.11.2",
49
49
  "openapi-types": "^12.1.3"
50
50
  },
51
- "peerDependenciesMeta": {
52
- "@hono/standard-validator": {
53
- "optional": true
54
- },
55
- "hono": {
56
- "optional": true
57
- }
51
+ "peerDependencies": {
52
+ "hono": "^4.11.2"
58
53
  },
59
54
  "devDependencies": {
60
55
  "@biomejs/biome": "^2.0.6",
61
- "@standard-schema/spec": "^1.0.0",
62
56
  "@valibot/to-json-schema": "^1.3.0",
63
57
  "arktype": "^2.1.22",
64
- "effect": "^3.17.13",
58
+ "effect": "^3.17.14",
65
59
  "husky": "^9.1.7",
66
60
  "is-ci": "^4.1.0",
67
61
  "nano-staged": "^0.8.0",