Repository: RobinTail/express-zod-api
Pull request #8 (2021-05-02 19:45:21)

Intersection and union


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8b6793f9..df2f4109 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,9 @@
 
 ## Version 0
 
+### v0.6.0
+- OpenAPI / Swagger specification generator now supports `ZodNullable`, `ZodOptional`, `ZodUnion` and `ZodIntersection` properties. 
+
 ### v0.5.0
 - More convenient way to attach routing to your custom express app
 - `ConfigType` changes:
diff --git a/README.md b/README.md
index 83e273a9..0e1bc5f9 100644
--- a/README.md
+++ b/README.md
@@ -25,7 +25,7 @@ Start your API server with I/O schema validation and custom middlewares in minut
 # Technologies
 
 - [Typescript](https://www.typescriptlang.org/) first
-- Schema validation — [Zod 3.x](https://github.com/colinhacks/zod).
+- Schema validation — [Zod 3.x](https://github.com/colinhacks/zod/tree/v3).
 - Webserver — [Express.js](https://expressjs.com/).
 - Logger — [Winston](https://github.com/winstonjs/winston).
 - Swagger - [OpenAPI 3.x](https://github.com/metadevpro/openapi3-ts)
diff --git a/example/example.swagger.yaml b/example/example.swagger.yaml
index 358c108e..4a9150d5 100644
--- a/example/example.swagger.yaml
+++ b/example/example.swagger.yaml
@@ -1,7 +1,7 @@
 openapi: 3.0.0
 info:
   title: Example API
-  version: 0.5.0
+  version: 0.6.0
 paths:
   /v1/getUser:
     get:
diff --git a/package.json b/package.json
index 4f9a746b..11a9b0e5 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "express-zod-api",
-  "version": "0.5.0",
+  "version": "0.6.0",
   "description": "Express Zod API",
   "license": "MIT",
   "scripts": {
@@ -15,7 +15,7 @@
     "lint": "yarn eslint ./src ./example ./tests",
     "precommit": "yarn lint && yarn test && yarn build:cleanup && yarn build:swagger && git add example/example.swagger.yaml",
     "prepublishOnly": "yarn test && yarn build",
-    "postversion": "git push --tags",
+    "postversion": "git push && git push --tags",
     "install_hooks": "husky install"
   },
   "main": "dist/index.js",
diff --git a/src/open-api.ts b/src/open-api.ts
index d43dd434..ab237db7 100644
--- a/src/open-api.ts
+++ b/src/open-api.ts
@@ -68,9 +68,20 @@ const getOpenApiPropertyType = (value: z.ZodTypeAny): Partial<SchemaObject> => {
         ...otherProps,
         ...getOpenApiPropertyType((value as z.ZodOptional<z.ZodTypeAny> | z.ZodNullable<z.ZodTypeAny>).unwrap())
       };
-    case value instanceof z.ZodUndefined:
-    case value instanceof z.ZodUnion:
     case value instanceof z.ZodIntersection:
+      return {
+        ...otherProps,
+        allOf: [
+          getOpenApiPropertyType((value as z.ZodIntersection<z.ZodTypeAny, z.ZodTypeAny>)._def.left),
+          getOpenApiPropertyType((value as z.ZodIntersection<z.ZodTypeAny, z.ZodTypeAny>)._def.right)
+        ]
+      };
+    case value instanceof z.ZodUnion:
+      return {
+        ...otherProps,
+        oneOf: (value as z.ZodUnion<[z.ZodTypeAny, ...z.ZodTypeAny[]]>)._def.options.map(getOpenApiPropertyType)
+      };
+    case value instanceof z.ZodUndefined:
     case value instanceof z.ZodTuple:
     case value instanceof z.ZodMap:
     case value instanceof z.ZodFunction:


Test patch:
diff --git a/tests/unit/__snapshots__/open-api.spec.ts.snap b/tests/unit/__snapshots__/open-api.spec.ts.snap
index 1fe19f19..fe84b024 100644
--- a/tests/unit/__snapshots__/open-api.spec.ts.snap
+++ b/tests/unit/__snapshots__/open-api.spec.ts.snap
@@ -63,7 +63,83 @@ servers:
 "
 `;
 
-exports[`Open API generator generateOpenApi() should generate the correct schema nullable and optional types 1`] = `
+exports[`Open API generator generateOpenApi() should generate the correct schema for intersection type 1`] = `
+"openapi: 3.0.0
+info:
+  title: Testing Intersection and And types
+  version: 3.4.5
+paths:
+  /v1/getSomething:
+    post:
+      responses:
+        default:
+          description: Successful response
+          content:
+            application/json:
+              schema:
+                $ref: \\"#/components/schemas/responseSchema1\\"
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: \\"#/components/schemas/requestBody1\\"
+components:
+  schemas:
+    responseSchema1:
+      type: object
+      properties:
+        and:
+          allOf:
+            - type: object
+              properties:
+                five:
+                  type: number
+              required:
+                - five
+            - type: object
+              properties:
+                six:
+                  type: string
+              required:
+                - six
+      required:
+        - and
+      description: /v1/getSomething POST response schema
+    requestBody1:
+      type: object
+      properties:
+        intersection:
+          allOf:
+            - type: object
+              properties:
+                one:
+                  type: string
+              required:
+                - one
+            - type: object
+              properties:
+                two:
+                  type: string
+              required:
+                - two
+      required:
+        - intersection
+      description: /v1/getSomething POST request body
+  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 nullable and optional types 1`] = `
 "openapi: 3.0.0
 info:
   title: Testing Nullable and Optional Types
@@ -204,3 +280,75 @@ servers:
   - url: http://example.com
 "
 `;
+
+exports[`Open API generator should generate the correct schema for union type 1`] = `
+"openapi: 3.0.0
+info:
+  title: Testing Union and Or Types
+  version: 3.4.5
+paths:
+  /v1/getSomething:
+    post:
+      responses:
+        default:
+          description: Successful response
+          content:
+            application/json:
+              schema:
+                $ref: \\"#/components/schemas/responseSchema1\\"
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: \\"#/components/schemas/requestBody1\\"
+components:
+  schemas:
+    responseSchema1:
+      type: object
+      properties:
+        or:
+          oneOf:
+            - type: string
+            - type: number
+      required:
+        - or
+      description: /v1/getSomething POST response schema
+    requestBody1:
+      type: object
+      properties:
+        union:
+          oneOf:
+            - type: object
+              properties:
+                one:
+                  type: string
+                two:
+                  type: number
+              required:
+                - one
+                - two
+            - type: object
+              properties:
+                two:
+                  type: number
+                three:
+                  type: string
+              required:
+                - two
+                - three
+      required:
+        - union
+      description: /v1/getSomething POST request body
+  responses: {}
+  parameters: {}
+  examples: {}
+  requestBodies: {}
+  headers: {}
+  securitySchemes: {}
+  links: {}
+  callbacks: {}
+tags: []
+servers:
+  - url: http://example.com
+"
+`;
diff --git a/tests/unit/open-api.spec.ts b/tests/unit/open-api.spec.ts
index ed557cbd..fd8a5ed0 100644
--- a/tests/unit/open-api.spec.ts
+++ b/tests/unit/open-api.spec.ts
@@ -43,7 +43,7 @@ describe('Open API generator', () => {
       expect(spec).toMatchSnapshot();
     });
 
-    test('should generate the correct schema nullable and optional types', () => {
+    test('should generate the correct schema for nullable and optional types', () => {
       const spec = new OpenAPI({
         routing: {
           v1: {
@@ -67,5 +67,78 @@ describe('Open API generator', () => {
       }).builder.getSpecAsYaml();
       expect(spec).toMatchSnapshot();
     });
+
+    test('should generate the correct schema for intersection type', () => {
+      const spec = new OpenAPI({
+        routing: {
+          v1: {
+            getSomething: endpointsFactory.build({
+              methods: ['post'],
+              input: z.object({
+                intersection: z.intersection(
+                  z.object({
+                    one: z.string(),
+                  }),
+                  z.object({
+                    two: z.string(),
+                  })
+                ),
+              }),
+              output: z.object({
+                and: z.object({
+                  five: z.number(),
+                }).and(z.object({
+                  six: z.string()
+                })),
+              }),
+              handler: async () => ({
+                and: {
+                  five: 5,
+                  six: 'six'
+                }
+              })
+            })
+          }
+        },
+        version: '3.4.5',
+        title: 'Testing Intersection and And types',
+        serverUrl: 'http://example.com'
+      }).builder.getSpecAsYaml();
+      expect(spec).toMatchSnapshot();
+    });
+  });
+
+  test('should generate the correct schema for union type', () => {
+    const spec = new OpenAPI({
+      routing: {
+        v1: {
+          getSomething: endpointsFactory.build({
+            methods: ['post'],
+            input: z.object({
+              union: z.union([
+                z.object({
+                  one: z.string(),
+                  two: z.number()
+                }),
+                z.object({
+                  two: z.number(),
+                  three: z.string()
+                })
+              ]),
+            }),
+            output: z.object({
+              or: z.string().or(z.number()),
+            }),
+            handler: async () => ({
+              or: 554
+            })
+          })
+        }
+      },
+      version: '3.4.5',
+      title: 'Testing Union and Or Types',
+      serverUrl: 'http://example.com'
+    }).builder.getSpecAsYaml();
+    expect(spec).toMatchSnapshot();
   });
 });