astahmer / openapi-zod-client

Generate a zodios (typescript http client with zod validation) from an OpenAPI spec (json/yaml)
openapi-zod-client.vercel.app
760 stars 82 forks source link

$ref is not resolved when path contains "~1" (escaped slash) #152

Open ki504178 opened 1 year ago

ki504178 commented 1 year ago

Hello! I would like to thank you in advance for the availability of a great library.

This Issue as well as the following vscode-openapi Issue, https://github.com/42Crunch/vscode-openapi/issues/34 If $ref contains ~1, the following error occurs and the client cannot be generated.

... /openapi-zod-client/dist/generateZodClientFromOpenAPI-6503eccc.cjs.dev.js:1100
        throw new Error("Schema ".concat(schema.$ref, " not found"));
              ^

Error: Schema #/paths/~1api~1hoge/fuga not found
astahmer commented 1 year ago

good catch ! feel free to open a PR to fix this 🙏 I think you'll have to update some places where normalizeString is used and replace it with a function that can handle those ~1

probably here https://github.com/astahmer/openapi-zod-client/blob/bda6ecb0a03145d7641c0e41c8aee982461401bc/lib/src/makeSchemaResolver.ts#L35

AkifumiSato commented 1 year ago

OpenAPI $ref can contain ~1, but not /, so it seems necessary to escape to ~1. https://swagger.io/docs/specification/using-ref/#escape

Error: Schema #/paths/~1api~1hoge/fuga not found

The above example contains /fuga. If I understand correctly, this error is in accordance with the specifications. But I am not sure of my understanding. What do you think?

AkifumiSato commented 1 year ago

Incidentally, the following tests were successful

test code ```ts import type { OpenAPIObject } from "openapi3-ts"; import { expect, test } from "vitest"; import { generateZodClientFromOpenAPI } from "../src"; // https://github.com/astahmer/openapi-zod-client/issues/152 test("ref-has-escape-string", async () => { const openApiDoc: OpenAPIObject = { openapi: "3.0.2", info: { title: "`$ref` has escape string", version: "v1", }, paths: { "/test": { get: { parameters: [ { name: "ref_has_escape_string", schema: { $ref: "#/components/schemas/~1component~1" }, in: "query", }, ], }, }, }, components: { schemas: { "~1component~1": { title: "MyComponent", enum: ["one", "two", "three"], type: "string", }, }, }, }; const output = await generateZodClientFromOpenAPI({ disableWriteToFile: true, openApiDoc }); expect(output).toMatchInlineSnapshot(` "import { makeApi, Zodios, type ZodiosOptions } from "@zodios/core"; import { z } from "zod"; const endpoints = makeApi([ { method: "get", path: "/test", requestFormat: "json", parameters: [ { name: "ref_has_escape_string", type: "Query", schema: z.enum(["one", "two", "three"]).optional(), }, ], response: z.void(), }, ]); export const api = new Zodios(endpoints); export function createApiClient(baseUrl: string, options?: ZodiosOptions) { return new Zodios(baseUrl, endpoints, options); } " `); }); ```
drew-benson-stash commented 11 months ago

I am experiencing the same issue. The problem is that there isn't actually a ~1 in the path - there is a / in the path, which is allowed, ex:

   ResponseBody:
      content:
        application/json:
          schema:
            $ref: './responses/errors.yml#/components/schemas/GenericErrorObject'

The / in application/json is automatically replaced with a ~1 by json-schema-ref-parser (in accordance with RFC 6901)

The problem is that later, in at least two places, that $1 is not being unescaped back to a / when trying to locate the schema.

Here is one example I was able to locate:

const map = get(doc, path.replace("#/", "").replace("#", "").replaceAll("/", ".")) ?? ({} as any);

The / delimited path is being converted to a . delimited path to be used by an underscore-style .get function, but since the ~1 isn't reverted back into a /, it can't find the reference. I was able to make this work by adding .replaceAll("~1", "/") but that's problematic if there actually is a ~1 in the path, and I'm still getting another error so it's not the only place it needs to be fixed.

I think the proper solution would be to use a decoding function from json-schema-ref-parser, like this one, but the hard part will be finding everywhere it needs to be used.

dgadelha commented 6 months ago

If anyone's also lost working around this issue, you can preprocess your OpenAPI document with the dereference method from the library mentioned in the previous comment:

import $RefParser from "@apidevtools/json-schema-ref-parser";
import { generateZodClientFromOpenAPI } from "openapi-zod-client";

const doc = { /* Your OpenAPI docs here */ };
const schema = await $RefParser.dereference(doc);

await generateZodClientFromOpenAPI({
  openApiDoc: schema,
  distPath: 'api.ts',
  options: {
    withAlias: true,
    withDescription: true,
  },
});