StefanTerdell / zod-to-json-schema

Converts Zod schemas to Json schemas
ISC License
854 stars 67 forks source link

reference root level schemas only #88

Closed sugaaloop closed 5 months ago

sugaaloop commented 11 months ago

Hey! Awesome library, I appreciate you building it.

We're using this with openapi-generator to build schemas/types with zod in our backend, create the openapi spec, then pull that down into the frontend to generate the axios api reference and common types. The issue is that the generator gets confused when the spec references a property on a shared schema. For instance:

{
  "openapi": "3.0.3",
  "components": {
    "schemas": {
      "someObject": {
        "type": "object",
        "properties": {
          "id": {
            "type": "number"
          },
          "name": {
            "type": "string"
   }}}}},
  "paths": {
    "/some-object/{id}": {
      "get": {
        "parameters": [
          {
            "schema": {

 busted up ->  "$ref": "#/components/schemas/someObject/properties/id"

            },
            "in": "path",
            "name": "id",
            "required": true
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {

 works fine ->  "$ref": "#/components/schemas/someObject"

}}}}}}}}}

The $ref for someObject works fine, but the $ref for someObject/properties/id gets corrupted by the generator. To be clear, the openapi schema is valid.

I'd rather any references only be to a schema on the top level of the components/schemas collection anyway, so my solution is to create a referenceStrategy that ignores any reference with "properties" in the path.

This seemed to work pretty well, if you're interested in it I'd be happy to put together a PR with tests and update the docs, if not I'll just use it as a patch.

// parseDef.ts

...

const get$ref = (
  item: Seen,
  refs: Refs
):
  | {
      $ref: string;
    }
  | {}
  | undefined => {
  switch (refs.$refStrategy) {
    case "root":
      return {
        $ref: getRootPath(item.path),
      };
    case "relative":
      return { $ref: getRelativePath(refs.currentPath, item.path) };
    case "none": {
      if (
        item.path.length < refs.currentPath.length &&
        item.path.every((value, index) => refs.currentPath[index] === value)
      ) {
        console.warn(
          `Recursive reference detected at ${refs.currentPath.join(
            "/"
          )}! Defaulting to any`
        );
        return {};
      }

      return undefined;
    }
    case "seen": {
      if (
        item.path.length < refs.currentPath.length &&
        item.path.every((value, index) => refs.currentPath[index] === value)
      ) {
        console.warn(
          `Recursive reference detected at ${refs.currentPath.join(
            "/"
          )}! Defaulting to any`
        );
        return {};
      } else {
        return item.jsonSchema;
      }
    }

    // new stuff here
    case "topLevelOnly": {
      if (item.path.includes('properties')) {
        return undefined;
      }
      return {
          $ref: getRootPath(item.path),
      };
    }
  }
};

// new stuff here
const getRootPath = (path: string[]) => {
  return path.length === 0
    ? ""
    : path.length === 1
    ? `${path[0]}/`
    : path.join("/");
};

...
StefanTerdell commented 9 months ago

The $ref for someObject works fine, but the $ref for someObject/properties/id gets corrupted by the generator. To be clear, the openapi schema is valid.

Sound like this should be an issue for the generator? Unless I'm missing something

StefanTerdell commented 9 months ago

@sugaaloop You know what, sure, why not :) Throw this in a PR and I will merge it.