Repository: RobinTail/express-zod-api
Pull request #336 (2022-02-26 20:38:46)

Discriminator type handling
Related to #334 . Supporting the new type introduced in Zod 3.12.0


Patch:
diff --git a/src/common-helpers.ts b/src/common-helpers.ts
index 411a2bea..6e87cf21 100644
--- a/src/common-helpers.ts
+++ b/src/common-helpers.ts
@@ -25,6 +25,7 @@ type Extractable =
 type UnionSchema = z.ZodUnion<[ObjectSchema, ...ObjectSchema[]]>;
 type IntersectionSchema = z.ZodIntersection<ObjectSchema, ObjectSchema>;
 
+// @todo should support DiscriminatedUnion too
 export type IOSchema = ObjectSchema | UnionSchema | IntersectionSchema;
 
 export type ArrayElement<T extends readonly unknown[]> =
@@ -112,6 +113,7 @@ export function combineEndpointAndMiddlewareInputSchemas<
   return result;
 }
 
+// @todo substr replace .substr() with an alternative
 function areFilesAvailable(request: Request) {
   const contentType = request.header("content-type") || "";
   const isMultipart =
diff --git a/src/open-api-helpers.ts b/src/open-api-helpers.ts
index 3044d34a..5a0c029c 100644
--- a/src/open-api-helpers.ts
+++ b/src/open-api-helpers.ts
@@ -104,17 +104,25 @@ export const depictFile: DepictHelper<ZodFile> = ({
 
 export const depictUnion: DepictHelper<
   z.ZodUnion<[z.ZodTypeAny, ...z.ZodTypeAny[]]>
-> = ({
-  schema: {
-    _def: { options },
-  },
-  initial,
-  isResponse,
-}) => ({
+> = ({ schema: { options }, initial, isResponse }) => ({
   ...initial,
   oneOf: options.map((option) => depictSchema({ schema: option, isResponse })),
 });
 
+export const depictDiscriminatedUnion: DepictHelper<
+  z.ZodDiscriminatedUnion<string, z.Primitive, z.ZodObject<any>>
+> = ({ schema: { options, discriminator }, initial, isResponse }) => {
+  return {
+    ...initial,
+    discriminator: {
+      propertyName: discriminator,
+    },
+    oneOf: Array.from(options.values()).map((option) =>
+      depictSchema({ schema: option, isResponse })
+    ),
+  };
+};
+
 export const depictIntersection: DepictHelper<
   z.ZodIntersection<z.ZodTypeAny, z.ZodTypeAny>
 > = ({
@@ -587,6 +595,7 @@ const depictHelpers: DepictingRules = {
   ZodEffects: depictEffect,
   ZodOptional: depictOptionalOrNullable,
   ZodNullable: depictOptionalOrNullable,
+  ZodDiscriminatedUnion: depictDiscriminatedUnion,
 };
 
 export const depictSchema: DepictHelper<z.ZodTypeAny> = ({


Test patch:
diff --git a/tests/unit/__snapshots__/open-api-helpers.spec.ts.snap b/tests/unit/__snapshots__/open-api-helpers.spec.ts.snap
index 5753bd68..3e45bb8c 100644
--- a/tests/unit/__snapshots__/open-api-helpers.spec.ts.snap
+++ b/tests/unit/__snapshots__/open-api-helpers.spec.ts.snap
@@ -75,6 +75,61 @@ Object {
 }
 `;
 
+exports[`Open API helpers depictDiscriminatedUnion() should depict ZodDiscriminatedUnion 1`] = `
+Object {
+  "description": "test",
+  "discriminator": Object {
+    "propertyName": "status",
+  },
+  "oneOf": Array [
+    Object {
+      "properties": Object {
+        "data": Object {
+          "format": "any",
+          "nullable": true,
+        },
+        "status": Object {
+          "enum": Array [
+            "success",
+          ],
+          "type": "string",
+        },
+      },
+      "required": Array [
+        "status",
+      ],
+      "type": "object",
+    },
+    Object {
+      "properties": Object {
+        "error": Object {
+          "properties": Object {
+            "message": Object {
+              "type": "string",
+            },
+          },
+          "required": Array [
+            "message",
+          ],
+          "type": "object",
+        },
+        "status": Object {
+          "enum": Array [
+            "error",
+          ],
+          "type": "string",
+        },
+      },
+      "required": Array [
+        "status",
+        "error",
+      ],
+      "type": "object",
+    },
+  ],
+}
+`;
+
 exports[`Open API helpers depictEffect() should depict ZodEffects preprocess in case of request 1`] = `
 Object {
   "format": "string (preprocessed)",
diff --git a/tests/unit/__snapshots__/open-api.spec.ts.snap b/tests/unit/__snapshots__/open-api.spec.ts.snap
index dc4e2f61..778e7c5d 100644
--- a/tests/unit/__snapshots__/open-api.spec.ts.snap
+++ b/tests/unit/__snapshots__/open-api.spec.ts.snap
@@ -1024,6 +1024,143 @@ servers:
 "
 `;
 
+exports[`Open API generator generateOpenApi() should generate the correct schema for discriminated union type 1`] = `
+"openapi: 3.0.0
+info:
+  title: Testing Discriminated Union Type
+  version: 3.4.5
+paths:
+  /v1/getSomething:
+    post:
+      responses:
+        \\"200\\":
+          description: POST /v1/getSomething Successful response
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  status:
+                    type: string
+                    enum:
+                      - success
+                  data:
+                    type: object
+                    properties:
+                      switch:
+                        discriminator:
+                          propertyName: status
+                        oneOf:
+                          - type: object
+                            properties:
+                              status:
+                                type: string
+                                enum:
+                                  - success
+                              data:
+                                nullable: true
+                                format: any
+                            required:
+                              - status
+                          - type: object
+                            properties:
+                              status:
+                                type: string
+                                enum:
+                                  - error
+                              error:
+                                type: object
+                                properties:
+                                  message:
+                                    type: string
+                                required:
+                                  - message
+                            required:
+                              - status
+                              - error
+                    required:
+                      - switch
+                required:
+                  - status
+                  - data
+        \\"400\\":
+          description: POST /v1/getSomething Error response
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  status:
+                    type: string
+                    enum:
+                      - error
+                  error:
+                    type: object
+                    properties:
+                      message:
+                        type: string
+                    required:
+                      - message
+                required:
+                  - status
+                  - error
+              examples:
+                example1:
+                  value:
+                    status: error
+                    error:
+                      message: Sample error message
+      requestBody:
+        content:
+          application/json:
+            schema:
+              description: POST /v1/getSomething request body
+              type: object
+              properties:
+                switch:
+                  discriminator:
+                    propertyName: type
+                  oneOf:
+                    - type: object
+                      properties:
+                        type:
+                          type: string
+                          enum:
+                            - a
+                        a:
+                          type: string
+                      required:
+                        - type
+                        - a
+                    - type: object
+                      properties:
+                        type:
+                          type: string
+                          enum:
+                            - b
+                        b:
+                          type: string
+                      required:
+                        - type
+                        - b
+              required:
+                - switch
+components:
+  schemas: {}
+  responses: {}
+  parameters: {}
+  examples: {}
+  requestBodies: {}
+  headers: {}
+  securitySchemes: {}
+  links: {}
+  callbacks: {}
+tags: []
+servers:
+  - url: http://example.com
+"
+`;
+
 exports[`Open API generator generateOpenApi() should generate the correct schema for intersection type 1`] = `
 "openapi: 3.0.0
 info:
diff --git a/tests/unit/open-api-helpers.spec.ts b/tests/unit/open-api-helpers.spec.ts
index 0d9de539..7d9e05b2 100644
--- a/tests/unit/open-api-helpers.spec.ts
+++ b/tests/unit/open-api-helpers.spec.ts
@@ -31,6 +31,7 @@ import {
   depictString,
   depictTuple,
   depictUnion,
+  depictDiscriminatedUnion,
   depictUpload,
   excludeExampleFromDepiction,
   excludeParamsFromDepiction,
@@ -169,6 +170,24 @@ describe("Open API helpers", () => {
     });
   });
 
+  describe("depictDiscriminatedUnion()", () => {
+    test("should depict ZodDiscriminatedUnion", () => {
+      expect(
+        depictDiscriminatedUnion({
+          schema: z.discriminatedUnion("status", [
+            z.object({ status: z.literal("success"), data: z.any() }),
+            z.object({
+              status: z.literal("error"),
+              error: z.object({ message: z.string() }),
+            }),
+          ]),
+          isResponse: false,
+          initial: { description: "test" },
+        })
+      ).toMatchSnapshot();
+    });
+  });
+
   describe("depictIntersection()", () => {
     test("should depict ZodIntersection", () => {
       expect(
diff --git a/tests/unit/open-api.spec.ts b/tests/unit/open-api.spec.ts
index d0b6ab5c..1243790d 100644
--- a/tests/unit/open-api.spec.ts
+++ b/tests/unit/open-api.spec.ts
@@ -168,6 +168,45 @@ describe("Open API generator", () => {
       expect(spec).toMatchSnapshot();
     });
 
+    // @todo move it to the top level
+    test("should generate the correct schema for discriminated union type", () => {
+      const spec = new OpenAPI({
+        config,
+        routing: {
+          v1: {
+            getSomething: defaultEndpointsFactory.build({
+              methods: ["post"],
+              input: z.object({
+                switch: z.discriminatedUnion("type", [
+                  z.object({ type: z.literal("a"), a: z.string() }),
+                  z.object({ type: z.literal("b"), b: z.string() }),
+                ]),
+              }),
+              output: z.object({
+                switch: z.discriminatedUnion("status", [
+                  z.object({ status: z.literal("success"), data: z.any() }),
+                  z.object({
+                    status: z.literal("error"),
+                    error: z.object({ message: z.string() }),
+                  }),
+                ]),
+              }),
+              handler: async () => ({
+                switch: {
+                  status: "success" as const,
+                  data: "test",
+                },
+              }),
+            }),
+          },
+        },
+        version: "3.4.5",
+        title: "Testing Discriminated Union Type",
+        serverUrl: "http://example.com",
+      }).getSpecAsYaml();
+      expect(spec).toMatchSnapshot();
+    });
+
     test("should handle transformation schema in output", () => {
       const spec = new OpenAPI({
         config,