Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[zod-openapi] Support ZodLazy #643

Open
Shyrogan opened this issue Jul 15, 2024 · 9 comments
Open

[zod-openapi] Support ZodLazy #643

Shyrogan opened this issue Jul 15, 2024 · 9 comments

Comments

@Shyrogan
Copy link

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)])
);
@Shyrogan Shyrogan changed the title [zod-openapi] Support ZodLazy types [zod-openapi] Support ZodLazy Jul 15, 2024
@shakibhasan09
Copy link

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

@coderhammer
Copy link

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

@Shyrogan
Copy link
Author

Shyrogan commented Aug 2, 2024

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

Sadly there is no other way than lazy

@coderhammer
Copy link

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
Copy link

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
Copy link
Author

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 !

@bernardoforcillo
Copy link

@yusukebe is this a problem inherited from @asteasolutions/zod-to-openapi?

@yusukebe
Copy link
Member

yusukebe commented Dec 4, 2024

Hi all.

I do not fully understand this problem. I think we can define the recursive types with the following code. Is not enough?

import { createRoute, OpenAPIHono } from '@hono/zod-openapi'
import { z } from '@hono/zod-openapi'

interface Category {
  name: string
  subcategories: Category[]
}

const CategorySchema: z.ZodSchema<Category> = z.lazy(() =>
  z.object({
    name: z.string(),
    subcategories: z.array(CategorySchema)
  })
)

const route = createRoute({
  method: 'post',
  path: '/',
  request: {
    body: {
      content: {
        'application/json': {
          schema: z.object({
            category: CategorySchema
          })
        }
      }
    }
  },
  responses: {
    200: {
      description: 'Return data'
    }
  }
})

const app = new OpenAPIHono()

app.openapi(route, (c) => {
  const data = c.req.valid('json')
  return c.json(data)
})

export default app

@yutakobayashidev
Copy link

yutakobayashidev commented Dec 4, 2024

this is an issue with @asteasolutions/zod-to-openapi. The maintainer has also suggested a similar workaround in the discussion. For now, I think using zod-to-json-schema is the best option.

asteasolutions/zod-to-openapi#255 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants