asteasolutions / zod-to-openapi

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

How to document a field, but not change the top level schema type? #245

Open marceloverdijk opened 4 days ago

marceloverdijk commented 4 days ago

When I have a schema like:

export const CircuitTypeSchema = z
  .enum([
    'RACE',
    'ROAD',
    'STREET'
  ])
  .openapi('CircuitType', { description: 'Represents a circuit type.' });

export const CircuitSchema = z
  .object({
    id: z.string().openapi({ description: 'The unique identifier.', example: 'melbourne' }),
    name: z.string().openapi({ description: 'The name.', example: 'Melbourne' }),
    type: CircuitTypeSchema, // .openapi({ description: 'The circuit type.', example: 'STREET' }),
  })
  .openapi('Circuit', { description: 'Represents a circuit.' });

this will generate a openapi spec like:

"components":{
  "schemas":{
    "CircuitType":{
      "type":"string",
      "enum":[
        "RACE",
        "ROAD",
        "STREET"
      ],
      "description":"Represents a circuit type."
    },
    "Circuit":{
      "type":"object",
      "properties":{
        "id":{
          "type":"string",
          "description":"The unique identifier.",
          "example":"melbourne"
        },
        "name":{
          "type":"string",
          "description":"The name.",
          "example":"Melbourne"
        },
        "type":{
          "$ref":"#/components/schemas/CircuitType"
        },
      },
      "required":[
        "id",
        "name",
        "type"      ],
      "description":"Represents a circuit."
    }
  },

which is good except, that I explicitly want to set the description and example for the Circuit.type field (which is now not specified.

So I tried with:

type: CircuitTypeSchema.openapi({ description: 'The circuit type.', example: 'STREET' }),

which unfortunately does not change the Circuit.type but the top-level CircuitType schema only:

"CircuitType": {
  "type": "string",
  "enum": [
    "RACE",
    "ROAD",
    "STREET"
  ],
  "description": "The circuit type.", <== Should be 'Represents the circuit type.'
  "example": "STREET" <== I don't want an example here.
},

and the Circuit.type:

    "Circuit":{
      "type":"object",
      "properties":{
        ..
        "type":{
          "$ref":"#/components/schemas/CircuitType"
        },

Is there a way to document (openapi) the Circuit.type field, bit not affect the top-level schema type?

marceloverdijk commented 4 days ago

So just to elaborate, I want to setup the schema so this is generated into the openapi spec file:

"components": {
  "schemas": {
    "CircuitType": {
      "type": "string",
      "enum": [
        "RACE",
        "ROAD",
        "STREET"
      ],
      "description": "Represents a circuit type."
    },
    "Circuit": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "description": "The unique identifier.",
          "example": "melbourne"
        },
        "name": {
          "type": "string",
          "description": "The name.",
          "example": "Melbourne"
        },
        "type": {
          "$ref": "#/components/schemas/CircuitType",
          "description": "The circuit type.",
          "example": "STREET"
        },
        ..
      },
      "required": [..],
      "description": "Represents a circuit."
    },
    ..
AGalabov commented 1 day ago

@marceloverdijk thank you for bringing this up. However I am unable to reproduce. Locally what I test with is:

import { z } from 'zod';
import { extendZodWithOpenApi } from './zod-extensions';
import { OpenApiGeneratorV3 } from './v3.0/openapi-generator';

extendZodWithOpenApi(z);

export const CircuitTypeSchema = z
  .enum(['RACE', 'ROAD', 'STREET'])
  .openapi('CircuitType', { description: 'Represents a circuit type.' });

export const CircuitSchema = z
  .object({
    id: z
      .string()
      .openapi({ description: 'The unique identifier.', example: 'melbourne' }),
    name: z
      .string()
      .openapi({ description: 'The name.', example: 'Melbourne' }),
    type: CircuitTypeSchema.openapi({
      description: 'The circuit type.',
      example: 'STREET',
    }),
  })
  .openapi('Circuit', { description: 'Represents a circuit.' });

const generator = new OpenApiGeneratorV3([CircuitTypeSchema, CircuitSchema]);
const doc = generator.generateDocument({} as never);

console.log(JSON.stringify(doc, null, 4));

And the resulting JSON looks like this:

{
    "components": {
        "schemas": {
            "CircuitType": {
                "type": "string",
                "enum": [
                    "RACE",
                    "ROAD",
                    "STREET"
                ],
                "description": "Represents a circuit type."
            },
            "Circuit": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "string",
                        "description": "The unique identifier.",
                        "example": "melbourne"
                    },
                    "name": {
                        "type": "string",
                        "description": "The name.",
                        "example": "Melbourne"
                    },
                    "type": {
                        "allOf": [
                            {
                                "$ref": "#/components/schemas/CircuitType"
                            },
                            {
                                "description": "The circuit type.",
                                "example": "STREET"
                            }
                        ]
                    }
                },
                "required": [
                    "id",
                    "name",
                    "type"
                ],
                "description": "Represents a circuit."
            }
        },
        "parameters": {}
    },
    "paths": {}
}

We've implemented the allOf logic since what you suggest:

 "type": {
          "$ref": "#/components/schemas/CircuitType",
          "description": "The circuit type.",
          "example": "STREET"
        },

is not valid according to the OpenAPI specification.

Can you please double check your example

marceloverdijk commented 1 day ago

Interesting and thx for your feedback.

In my case it overwrites the description and add the example to the CircuitType schema definition and for the type field in the Circuit schema it does not use anyOf but just the "type": { "$ref": "#/components/schemas/CircuitType" }.

I checked my project and it uses the latest 7.1.1:

"node_modules/@asteasolutions/zod-to-openapi": {
      "version": "7.1.1",
      "resolved": "https://registry.npmjs.org/@asteasolutions/zod-to-openapi/-/zod-to-openapi-7.1.1.tgz",

but my setup is different. I'm targeting openapi spec 3.1 and I'm using Cloudflare Chanfana. Maybe that's causing some impact as Chanfana is using zod/openapi as well (https://github.com/asteasolutions/zod-to-openapi/issues/234#issuecomment-2198022860)