asteasolutions / zod-to-openapi

A library that generates OpenAPI (Swagger) docs from Zod schemas
MIT License
983 stars 60 forks source link

Header Description Not at the Top Level in OpenAPI Specification #270

Open cosmin-ghitea opened 3 hours ago

cosmin-ghitea commented 3 hours ago

I encountered an issue while using @asteasolutions/zod-to-openapi. When I define custom headers with the .openapi() method in zod, the description for each header is nested under schema. I expect the description to be at the same level as name, in accordance with the OpenAPI 3.1 specification.

import { extendZodWithOpenApi, OpenApiGeneratorV31 } from "@asteasolutions/zod-to-openapi";
import { z } from "zod";
import * as yaml from "yaml";
import * as fs from "fs";

extendZodWithOpenApi(z);

const headersSchema = z.object({
  "Header_A": z.string().optional().openapi({
    description: "Access token. Example: `123`",
  }),
  "Header_B": z.enum(["ENUM1", "ENUM2"]).openapi({
    description: "Type of the device making the request. Example: `123`",
  }),
});

// Define response schema
const responseSchema = z.object({
  message: z.string().openapi({
    description: "A success message.",
  }),
});

import { OpenAPIRegistry } from "@asteasolutions/zod-to-openapi";
const registry = new OpenAPIRegistry();

registry.registerPath({
  method: "get",
  path: "/example",
  summary: "Example endpoint",
  description: "An example endpoint to test header generation.",
  request: {
    headers: headersSchema,
  },
  responses: {
    200: {
      description: "Success",
      content: {
        "application/json": {
          schema: responseSchema,
        },
      },
    },
  },
});

// Generate OpenAPI document
const generator = new OpenApiGeneratorV31(registry.definitions);

const openApiDocument = generator.generateDocument({
  openapi: "3.1.0",
  info: {
    version: "1.0.0",
    title: "Example API",
    description: "A small example demonstrating @asteasolutions/zod-to-openapi.",
  },
});

const fileContent = yaml.stringify(openApiDocument);

fs.writeFileSync(`./openapi-docs.yml`, fileContent, {
  encoding: "utf-8",
});

console.log(JSON.stringify(openApiDocument, null, 2));

The generated OpenAPI currently nests the description inside schema

openapi: 3.1.0
info:
  version: 1.0.0
  title: Example API
  description: A small example demonstrating @asteasolutions/zod-to-openapi.
paths:
  /example:
    get:
      summary: Example endpoint
      description: An example endpoint to test header generation.
      parameters:
        - name: Header_A
          required: false
          in: header
          schema:
            type: string
            description: "Access token. Example: `123`"
        - name: Header_B
          required: true
          in: header
          schema:
            type: string
            enum:
              - ENUM1
              - ENUM2
            description: "Type of the device making the request. Example: `123`"
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    description: A success message.
                required:
                  - message
AGalabov commented 3 hours ago

Please see https://github.com/asteasolutions/zod-to-openapi?tab=readme-ov-file#defining-route-parameters. It might not be obvious but in order to put any metadata at the param level (and not the schema level) you have to nest it like so:

param: {
  description: 'your description here'
}
cosmin-ghitea commented 2 hours ago

So in my case something like this

z.string().optional().openapi({
    param: {
      description: "Access token. Example: `123`",
    }
  }),
AGalabov commented 2 hours ago

Yes