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

Followed the example and failed #17

Closed
nazarEnzo opened this issue Jun 17, 2022 · 8 comments · Fixed by #18
Closed

Followed the example and failed #17

nazarEnzo opened this issue Jun 17, 2022 · 8 comments · Fixed by #18

Comments

@nazarEnzo
Copy link

nazarEnzo commented Jun 17, 2022

Hi. I followed the example and got the following error:

UnknownZodTypeError {
  message: 'Unknown zod object type, please specify `type` and other OpenAPI props using `ZodSchema.openapi`.',
  data: {
    currentSchema: {
      shape: [Function: shape],
      unknownKeys: 'strip',
      catchall: [ZodNever],
      typeName: 'ZodObject',
      openapi: [Object]
    },
    schemaName: 'User'
  }
}

when trying to use
registry.registerParameter,
registry.register,
registry.registerPath in NextJS api route.

My package.json:

"dependencies": {
    "@asteasolutions/zod-to-openapi": "^1.2.1",
}

My code:

import { extendZodWithOpenApi, OpenAPIGenerator, OpenAPIRegistry } from '@asteasolutions/zod-to-openapi';
import { NextApiRequest, NextApiResponse } from 'next';
import { z } from 'zod';

function getOpenApiDocumentation() {
  extendZodWithOpenApi(z);
  const registry = new OpenAPIRegistry();

  const UserIdSchema = registry.registerParameter(
    "UserId",
    z.string().openapi({
      param: {
        name: "id",
        in: "path",
      },
      example: "1212121",
    })
  );
  const UserSchema = registry.register(
    "User",
    z.object({
      id: z.string().openapi({
        example: "1212121",
      }),
      name: z.string().openapi({
        example: "John Doe",
      }),
      age: z.number().openapi({
        example: 42,
      }),
    })
  );

  registry.registerPath({
    method: "get",
    path: "/users/{id}",
    description: "Get user data by its id",
    summary: "Get a single user",
    request: {
      params: z.object({ id: UserIdSchema }),
    },
    responses: {
      200: {
        mediaType: "application/json",
        schema: UserSchema.openapi({
          description: "Object with user data.",
        }),
      },
      204: z
        .void()
        .openapi({ description: "No content - successful operation" }),
    },
  });

  const generator = new OpenAPIGenerator(registry.definitions);

  return generator.generateDocument({
    openapi: "3.0.0",
    info: {
      version: "1.0.0",
      title: "My API",
      description: "This is the API",
    },
    servers: [{ url: "localhost:3000/api" }],
  });
}

export default (req: NextApiRequest, res: NextApiResponse) => {
  return new Promise<void>((resolve, reject) => {
    try {
      const openApiDocumentation = getOpenApiDocumentation();
      res.status(200).json(openApiDocumentation);
      resolve();
    } catch (e) {
      console.error(e);
      reject();
    }
    resolve();
  });
};

What am I doing wrong?

@AGalabov
Copy link
Collaborator

Hello @nazarEnzo ,

I tried your code without the next dependency locally. Basically the exact same code with console.log(generateOpenAPISchemas()); at the end. When running it with ts-node it worked just as expected - logging the following response:

{
  openapi: '3.0.0',
  info: { version: '1.0.0', title: 'My API', description: 'This is the API' },
  servers: [ { url: 'localhost:3000/api' } ],
  components: { schemas: { User: [Object] }, parameters: { UserId: [Object] } },
  paths: { '/users/{id}': { get: [Object] } }
}

I see nothing in your code that would cause this error. Maybe some additional info like what command are you using to execute this code would help.

@nazarEnzo
Copy link
Author

nazarEnzo commented Jun 18, 2022

Hello @AGalabov,

Thank you for your reply!

I tried it with ts-node and it works. For some reason it is not functioning in the NextJs environment.

How to replicate:

  1. Create Next App: npx create-next-app@latest --ts exampleApp
  2. Install zod-to-openapi: npm install @asteasolutions/zod-to-openapi
  3. Go to pages/api/hello.ts and paste the following:
import { extendZodWithOpenApi, OpenAPIGenerator, OpenAPIRegistry } from '@asteasolutions/zod-to-openapi';
import { z } from 'zod';

import type { NextApiRequest, NextApiResponse } from "next";

function getOpenApiDocumentation() {
  extendZodWithOpenApi(z);
  const registry = new OpenAPIRegistry();

  const UserIdSchema = registry.registerParameter(
    "UserId",
    z.string().openapi({
      param: {
        name: "id",
        in: "path",
      },
      example: "1212121",
    })
  );
  const UserSchema = registry.register(
    "User",
    z.object({
      id: z.string().openapi({
        example: "1212121",
      }),
      name: z.string().openapi({
        example: "John Doe",
      }),
      age: z.number().openapi({
        example: 42,
      }),
    })
  );
  registry.registerPath({
    method: "get",
    path: "/users/{id}",
    description: "Get user data by its id",
    summary: "Get a single user",
    request: {
      params: z.object({ id: UserIdSchema }),
    },
    responses: {
      200: {
        mediaType: "application/json",
        schema: UserSchema.openapi({
          description: "Object with user data.",
        }),
      },
      204: z
        .void()
        .openapi({ description: "No content - successful operation" }),
    },
  });

  const generator = new OpenAPIGenerator(registry.definitions);

  return generator.generateDocument({
    openapi: "3.0.0",
    info: {
      version: "1.0.0",
      title: "My API",
      description: "This is the API",
    },
    servers: [{ url: "localhost:3000/api" }],
  });
}

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse<any>
) {
  try {
    res.status(200).json(getOpenApiDocumentation());
  } catch (error) {
    console.error(error);
    res.status(400).json(error);
  }
}

  1. Save and run with npm run dev
  2. Trigger endpoint - http://localhost:3000/api/hello

P.S It works without schemes, parameters and paths

Thank you!

@AGalabov
Copy link
Collaborator

AGalabov commented Jun 18, 2022

It seems that the problem is coming from next. The library is based on instanceof checks. I'd suggest checking what your target JS is from your webpack and TS configs. It should be ES6 or higher.

Alternatively - I would imagine that your common use case would not be to generate the documentation runtime (on each call of the api) but rather build time. So if you do that once (when building) with a script using ts-node you can save the result in a file that is to be served every time the API is hit.

@georgyangelov
Copy link
Collaborator

Just to expand on what @AGalabov mentioned, it's related to this issue: https://www.dannyguo.com/blog/how-to-fix-instanceof-not-working-for-custom-errors-in-typescript/

@georgyangelov
Copy link
Collaborator

georgyangelov commented Jun 18, 2022

Okay, so I investigated a bit and the problem seems to be different. Somehow the zod instances that (1) the lib uses (require('zod')) and (2) that is imported in next are different. require('zod') from lib !== require('zod') from next. This is very weird and most probably due to the extremely complicated build process of next/webpack. So this means that there are actually (at least) two instances of classes like ZodString which we're doing instanceof checks against. I'll try to look into it more tomorrow.

@georgyangelov
Copy link
Collaborator

georgyangelov commented Jun 18, 2022

@AGalabov If I don't find a config change that helps this case the fallback fix would be to use error.constructor.name === 'ZodSomething' instead of error instanceof ZodSomething, which is a shame, but ... well, JS...

@georgyangelov
Copy link
Collaborator

georgyangelov commented Jun 19, 2022

I figured out the problem. Just need to figure out the solution now :)


Here's some technical stuff if someone is interested

Looking at the compiled code emitted by webpack, I see this:

exports.modules = {

/***/ 776:
/***/ ((module) => {

module.exports = require("@asteasolutions/zod-to-openapi");

/***/ }),

/***/ 926:
/***/ ((module) => {

module.exports = import("zod");;

/***/ }),

Notice how @asteasolutions/zod-to-openapi is required and zod is imported. Remember this is compiled code so it's directly executed by nodejs. This is important as require is the CJS way of importing JS and import imports modules. Those two are separate and given zod's package.json:

"main": "./lib/index.js",
  "types": "./index.d.ts",
  "module": "./lib/index.mjs",
  "dependencies": {},
  "exports": {
    ".": {
      "require": "./lib/index.js",
      "import": "./lib/index.mjs",
      "types": "./index.d.ts"
    },
    "./package.json": "./package.json"
  },

we see that require loads lib/index.js (commonjs module), while import loads lib/index.mjs (ECMAScript module). Those are different files => they are different instances (of the same code but that doesn't matter, it's compiled differently). So since zod-to-openapi currently does not offer modules imports it's required as CJS. And CJS modules can only load other CJS modules => this leads to zod-to-openapi using a different instance of Zod, which leads to instanceof checks not working correctly since they are different classes (with the same name, but that doesn't matter).

So what happens is:

index.js -> require('zod-to-openapi') -> require('zod') (instance A)
         -> import('zod') (instance B)

Here's a test script to illustrate the problem:

async function test() {
  const zod1 = (await import('zod')).z.ZodString;
  const zod2 = require('zod').z.ZodString;

  console.log(zod1 === zod2);
}

test().catch(e => console.error(e));

The output is false, which makes sense when you think about it: different files => different instances


So solutions would be:

  1. Provide ES module support like Zod does, this would mean both code paths import zod as ES modules => they will use the same instances
  2. Switch to using schema.constructor.name or some other check instead of instanceof. This way we won't actually need to import concrete values from Zod, just types, which would mean we won't ever call require('zod') => that would solve the problem as well

It's probably best to implement both, I'll see if I can open a PR soon.

georgyangelov added a commit that referenced this issue Jun 19, 2022
The change is to make zod-to-openapi independent of a specific Zod instance.
See #17 (comment)
@georgyangelov
Copy link
Collaborator

@nazarEnzo Should be fixed in v1.2.2, can you please confirm? Thank you for the report and the reproduction steps

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

Successfully merging a pull request may close this issue.

3 participants