Repository: RobinTail/express-zod-api
Pull request #758 (2022-12-23 15:41:42)

Wrong listing of required object props in case of coercion in response
Found a bug introduced in 8.6.0


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 12bc84c1..6ff14b06 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,34 @@
 
 ## Version 8
 
+### v8.8.1
+
+- Fixed a bug introduced in v8.6.0.
+  - The list of required object properties was depicted incorrectly by the OpenAPI generator in case of using the new
+    `coerce` feature in the response schema.
+
+```typescript
+// reproduction example
+const endpoint = defaultEndpointsFactory.build({
+  // ...
+  output: z.object({
+    a: z.string(),
+    b: z.coerce.string(),
+    c: z.coerce.string().optional(),
+  }),
+});
+```
+
+```yaml
+before:
+  required:
+    - a
+after:
+  required:
+    - a
+    - b
+```
+
 ### v8.8.0
 
 - First step on generating better Typescript from your IO schemas.
diff --git a/src/open-api-helpers.ts b/src/open-api-helpers.ts
index 5b297e9d..82fff04a 100644
--- a/src/open-api-helpers.ts
+++ b/src/open-api-helpers.ts
@@ -178,9 +178,11 @@ export const depictObject: Depicter<z.AnyZodObject> = ({
   properties: depictObjectProperties({ schema, isResponse, next }),
   required: Object.keys(schema.shape).filter((key) => {
     const prop = schema.shape[key];
-    return isResponse && hasCoercion(prop)
-      ? prop instanceof z.ZodOptional
-      : !prop.isOptional();
+    const isOptional =
+      isResponse && hasCoercion(prop)
+        ? prop instanceof z.ZodOptional
+        : prop.isOptional();
+    return !isOptional;
   }),
 });
 


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 93048fdc..0030ba67 100644
--- a/tests/unit/__snapshots__/open-api-helpers.spec.ts.snap
+++ b/tests/unit/__snapshots__/open-api-helpers.spec.ts.snap
@@ -364,6 +364,28 @@ exports[`Open API helpers depictNumber() should type:number, min/max, format and
 }
 `;
 
+exports[`Open API helpers depictObject() Bug #758 1`] = `
+{
+  "properties": {
+    "a": {
+      "type": "string",
+    },
+    "b": {
+      "type": "string",
+    },
+    "c": {
+      "nullable": true,
+      "type": "string",
+    },
+  },
+  "required": [
+    "a",
+    "b",
+  ],
+  "type": "object",
+}
+`;
+
 exports[`Open API helpers depictObject() should type:object, properties and required props 0 1`] = `
 {
   "properties": {
@@ -425,6 +447,52 @@ exports[`Open API helpers depictObject() should type:object, properties and requ
       "type": "string",
     },
   },
+  "required": [
+    "a",
+    "b",
+  ],
+  "type": "object",
+}
+`;
+
+exports[`Open API helpers depictObject() should type:object, properties and required props 3 1`] = `
+{
+  "properties": {
+    "a": {
+      "exclusiveMaximum": false,
+      "exclusiveMinimum": false,
+      "format": "double",
+      "maximum": 1.7976931348623157e+308,
+      "minimum": 5e-324,
+      "type": "number",
+    },
+    "b": {
+      "type": "string",
+    },
+  },
+  "required": [
+    "a",
+  ],
+  "type": "object",
+}
+`;
+
+exports[`Open API helpers depictObject() should type:object, properties and required props 4 1`] = `
+{
+  "properties": {
+    "a": {
+      "exclusiveMaximum": false,
+      "exclusiveMinimum": false,
+      "format": "double",
+      "maximum": 1.7976931348623157e+308,
+      "minimum": 5e-324,
+      "type": "number",
+    },
+    "b": {
+      "nullable": true,
+      "type": "string",
+    },
+  },
   "required": [],
   "type": "object",
 }
diff --git a/tests/unit/open-api-helpers.spec.ts b/tests/unit/open-api-helpers.spec.ts
index 68483c6b..20d749ff 100644
--- a/tests/unit/open-api-helpers.spec.ts
+++ b/tests/unit/open-api-helpers.spec.ts
@@ -401,6 +401,14 @@ describe("Open API helpers", () => {
         isResponse: true,
         shape: { a: z.coerce.number(), b: z.string({ coerce: true }) },
       },
+      {
+        isResponse: true,
+        shape: { a: z.number(), b: z.string().optional() },
+      },
+      {
+        isResponse: false,
+        shape: { a: z.number().optional(), b: z.coerce.string() },
+      },
     ])(
       "should type:object, properties and required props %#",
       ({ shape, ...context }) => {
@@ -413,6 +421,21 @@ describe("Open API helpers", () => {
         ).toMatchSnapshot();
       }
     );
+
+    test("Bug #758", () => {
+      const schema = z.object({
+        a: z.string(),
+        b: z.coerce.string(),
+        c: z.coerce.string().optional(),
+      });
+      expect(
+        depictObject({
+          schema,
+          ...responseContext,
+          next: makeNext(responseContext),
+        })
+      ).toMatchSnapshot();
+    });
   });
 
   describe("depictNull()", () => {