asteasolutions / zod-to-openapi

A library that generates OpenAPI (Swagger) docs from Zod schemas
MIT License
955 stars 60 forks source link

Schema being inlined despite registering it #230

Open JeongJuhyeon opened 5 months ago

JeongJuhyeon commented 5 months ago

Hi, new user here. The output I'm getting contains no "$ref" whatsoever despite registering my schema. Any idea what's going wrong here? Fwiw, the generated OpenAPI definitions themselves are correct.

Input:


import { registry } from "../lib/openapi.ts";

export const ModificationRequest = z.object({
   // ...
});
registry.register("ModificationRequest", ModificationRequest);

  registry.registerPath({
    method: "post",
    path: "/modify",
    description: "Apply the given modifications",
    request: {
      body: {
        content: {
          "application/json": {
            schema: ModificationRequest,
          },
        },
        required: true,
      },
    },
    responses: {
      200: {
        description: "OK",
        content: {
          "application/json": {
            schema: { type: "object", properties: { OK: { type: "boolean" } } },
          },
        },
      },
    },
  });

Output:

openapi: 3.0.0
info:
  title: App API
  version: 1.0.0
components:
  schemas:
// ...
    ModificationRequest:
      type: object
      properties: 
// ...
paths:
  /modify:
    post:
      description: Apply the given modifications
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
// ...
AGalabov commented 5 months ago

@JeongJuhyeon, yes I know what's the issue:

registry.register is essentially returning a new schema that has some custom metadata applied to it. So the correct way to achieve what you wanted is to do:

export const ModificationRequest = registry.register("ModificationRequest", z.object({
   // ...
}););
// everything else should remain the self
JeongJuhyeon commented 5 months ago

@AGalabov Thanks for the help, this has improved it but part of the problem remains.

const A = registry.register("A", ...));
const B = registry.register("B"., ...));
const C = registry.register("C", ...));

const D = registry.register("D", z.union(
  [A, B, C]
  )
);

I'm getting the result

components:
  schemas:
    A: ...
    B: ...
    C: ...
    D: 
        anyOf:
            - type: object
            - ...

That is, A B and C don't get reused as part of D, their definitions are inlined again in D.

AGalabov commented 4 months ago

@JeongJuhyeon can you provide a full reproducible script? We have such a similar test case here: https://github.com/asteasolutions/zod-to-openapi/blob/master/spec/types/union.spec.ts#L26

JeongJuhyeon commented 4 months ago

@AGalabov Repro:

import { extendZodWithOpenApi, OpenApiGeneratorV3, OpenAPIRegistry } from "@asteasolutions/zod-to-openapi";
import YAML from "yaml";
import { z } from "zod";

extendZodWithOpenApi(z);

const registry = new OpenAPIRegistry();

const requestedModificationBaseSchema = z.object({
  target: z.enum(["apple", "banana", "grape"]),
});
const requestedModificationBase = registry.register("RequestedModificationBase", requestedModificationBaseSchema);

const RequestedTextModification = registry.register(
  "RequestedTextModification",
  z.union([z.object({ x: z.number() }), z.object({ y: z.number() })]),
);

const RequestedChairModification = registry.register(
    "RequestedChairModification",
    z.union([z.object({ z: z.number() }), z.object({ a: z.number() })]),
  );

const RequestedSubcategoryModificationSchema = z.union([RequestedTextModification, RequestedChairModification]);
const RequestedSubcategoryModification = registry.register(
  "RequestedSubcategoryModification",
  RequestedSubcategoryModificationSchema,
);

const ModificationRequestSchema = z.object({
  userPageId: z.string(),
  modifications: z.array(RequestedSubcategoryModification.and(requestedModificationBase)),
});

console.log("Registering request schema");
const ModificationRequest = registry.register("ModificationRequest", ModificationRequestSchema);

const generatedApiJson = new OpenApiGeneratorV3(registry.definitions).generateDocument({
    openapi: "3.0.0",
    info: { title: "App API", version: "1.0.0" },
});

console.log("Generated API JSON");
console.log(YAML.stringify(generatedApiJson));

Result:

openapi: 3.0.0
info:
  title: App API
  version: 1.0.0
components:
  schemas:
    RequestedModificationBase:
      type: object
      properties:
        target:
          type: string
          enum:
            - apple
            - banana
            - grape
      required:
        - target
    RequestedTextModification:
      anyOf:
        - type: object
          properties:
            x:
              type: number
          required:
            - x
        - type: object
          properties:
            y:
              type: number
          required:
            - y
    RequestedChairModification:
      anyOf:
        - type: object
          properties:
            z:
              type: number
          required:
            - z
        - type: object
          properties:
            a:
              type: number
          required:
            - a
    RequestedSubcategoryModification:
      anyOf:
        - type: object
          properties:
            x:
              type: number
          required:
            - x
        - type: object
          properties:
            y:
              type: number
          required:
            - y
        - type: object
          properties:
            z:
              type: number
          required:
            - z
        - type: object
          properties:
            a:
              type: number
          required:
            - a
    ModificationRequest:
      type: object
      properties:
        userPageId:
          type: string
        modifications:
          type: array
          items:
            allOf:
              - $ref: "#/components/schemas/RequestedSubcategoryModification"
              - $ref: "#/components/schemas/RequestedModificationBase"
      required:
        - userPageId
        - modifications
  parameters: {}
paths: {}

"RequestedChairModification" and "RequestedTextModification" are inlined in "RequestedSubcategoryModification" instead of being used as $ref.

AGalabov commented 1 month ago

@JeongJuhyeon sorry it is taking so long, but free time is hard to find.

I see your example and it turns out the problem is the fact that you are using a union inside the union. I see the problematic piece of code. I'll see what I can do about it.