honojs / middleware

monorepo for Hono third-party middleware/helpers/wrappers
https://hono.dev
474 stars 169 forks source link

[zod-openapi] Support ZodLazy #643

Open Shyrogan opened 4 months ago

Shyrogan commented 4 months ago

Similarly to how zod-openapi does it: https://github.com/samchungy/zod-openapi/tree/master?tab=readme-ov-file#supported-zod-schema

Currently, we get the following error:

Unknown zod object type, please specify type and other OpenAPI props using ZodSchema.openapi.

A good example of usage would be the JSON schema from Zod's documentation:

const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
type Literal = z.infer<typeof literalSchema>;
type Json = Literal | { [key: string]: Json } | Json[];
const jsonSchema: z.ZodType<Json> = z.lazy(() =>
  z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)])
);
shakibhasan09 commented 4 months ago

Any update so far? I'm facing the same issue

coderhammer commented 3 months ago

Facing the same issue, did you find a workaround to support recursive types?

Shyrogan commented 3 months ago

Facing the same issue, did you find a workaround to support recursive types?

Sadly there is no other way than lazy

coderhammer commented 3 months ago

I ended up overwriting type with the .openapi method like this:

BaseSearchOrdersFiltersSchema.extend({
    oneOf: z
      .lazy(() => SearchOrdersFiltersSchema.array().optional())
      .openapi({
        type: "array",
        items: {
          type: "object",
        },
      }),

I'm sure there is a better way to define the openapi schema but this at least does not break the spec generation

valerii15298 commented 1 month ago

I found the solution. You can use https://github.com/StefanTerdell/zod-to-json-schema Here is full example:

import { serve } from "@hono/node-server";
import { swaggerUI } from "@hono/swagger-ui";
import { createRoute, OpenAPIHono } from "@hono/zod-openapi";
import { z } from "@hono/zod-openapi";
import { zodToJsonSchema } from "zod-to-json-schema";

const baseCategorySchema = z.object({ name: z.string() });

type Category = z.infer<typeof baseCategorySchema> & {
  subcategories: Category[];
};

const categorySchemaRaw: z.ZodType<Category> = baseCategorySchema.extend({
  subcategories: z.lazy(() => categorySchemaRaw.array()),
});

const name = "Category";
const jsonSchema = zodToJsonSchema(categorySchemaRaw, {
  basePath: [`#/components/schemas/${name}`],
});
// console.dir(jsonSchema, { depth: null });
const schema = categorySchemaRaw.openapi(name, jsonSchema as {}).openapi({
  example: {
    name: "test1",
    subcategories: [
      {
        name: "test2",
        subcategories: [],
      },
    ],
  },
});

const app = new OpenAPIHono({ strict: false });

app.openapi(
  createRoute({
    method: "post",
    path: "/test",
    request: { body: { content: { "application/json": { schema } } } },
    responses: { 200: { description: "test" } },
  }),
  (c) => c.text("test"),
);

type OpenAPIObjectConfig = Parameters<typeof app.getOpenAPIDocument>[0];
const config: OpenAPIObjectConfig = {
  openapi: "3.0.3",
  info: { version: "0.0.1", title: "Some API" },
};
const pathOpenAPI = "/openapi";
app.doc(pathOpenAPI, config);

app.get("/swagger-ui", swaggerUI({ url: pathOpenAPI }));

serve({
  async fetch(req, env) {
    return app.fetch(req, env);
  },
  port: 4001,
});

// eslint-disable-next-line no-console
console.log(`Server is running on port 4001`);

// const schemaOpenAPI = app.getOpenAPIDocument(config);
// console.dir(schemaOpenAPI, { depth: null });

In swagger UI Category is correctly shown recursively with correct types instead of just any object.

Also this approach is very convenient to use when you have all your zodiac schemas in one file with a script that converts each zod schema to use openapi with zod-to-json-schema.

In my case I generate zod types from prisma schema using zod-prisma-types and since there are a lot of usage of zod.Lazy so I then run my own script to convert each zod schema to be as in example above(use json schema for openapi). The significant benefit of this approach is that you extract your reusable openapi schemas into #/components/schemas/ and it is displayed below in swagger ui editor.

Shyrogan commented 1 month ago

I found the solution. You can use https://github.com/StefanTerdell/zod-to-json-schema Here is full example:

import { serve } from "@hono/node-server";
import { swaggerUI } from "@hono/swagger-ui";
import { createRoute, OpenAPIHono } from "@hono/zod-openapi";
import { z } from "@hono/zod-openapi";
import { zodToJsonSchema } from "zod-to-json-schema";

const baseCategorySchema = z.object({ name: z.string() });

type Category = z.infer<typeof baseCategorySchema> & {
  subcategories: Category[];
};

const categorySchemaRaw: z.ZodType<Category> = baseCategorySchema.extend({
  subcategories: z.lazy(() => categorySchemaRaw.array()),
});

const name = "Category";
const jsonSchema = zodToJsonSchema(categorySchemaRaw, {
  basePath: [`#/components/schemas/${name}`],
});
// console.dir(jsonSchema, { depth: null });
const schema = categorySchemaRaw.openapi(name, jsonSchema as {}).openapi({
  example: {
    name: "test1",
    subcategories: [
      {
        name: "test2",
        subcategories: [],
      },
    ],
  },
});

const app = new OpenAPIHono({ strict: false });

app.openapi(
  createRoute({
    method: "post",
    path: "/test",
    request: { body: { content: { "application/json": { schema } } } },
    responses: { 200: { description: "test" } },
  }),
  (c) => c.text("test"),
);

type OpenAPIObjectConfig = Parameters<typeof app.getOpenAPIDocument>[0];
const config: OpenAPIObjectConfig = {
  openapi: "3.0.3",
  info: { version: "0.0.1", title: "Some API" },
};
const pathOpenAPI = "/openapi";
app.doc(pathOpenAPI, config);

app.get("/swagger-ui", swaggerUI({ url: pathOpenAPI }));

serve({
  async fetch(req, env) {
    return app.fetch(req, env);
  },
  port: 4001,
});

// eslint-disable-next-line no-console
console.log(`Server is running on port 4001`);

// const schemaOpenAPI = app.getOpenAPIDocument(config);
// console.dir(schemaOpenAPI, { depth: null });

In swagger UI Category is correctly shown recursively with correct types instead of just any object.

Also this approach is very convenient to use when you have all your zodiac schemas in one file with a script that converts each zod schema to use openapi with zod-to-json-schema.

In my case I generate zod types from prisma schema using zod-prisma-types and since there are a lot of usage of zod.Lazy so I then run my own script to convert each zod schema to be as in example above(use json schema for openapi). The significant benefit of this approach is that you extract your reusable openapi schemas into #/components/schemas/ and it is displayed below in swagger ui editor.

Thank you for this, will look into it :)

I still think it could be a great improvement for Hono !