Repository: RobinTail/express-zod-api (ts)

Issue:
`z.record(z.any())` generates invalid OpenAPI spec
Hi, I've run into some trouble with OpenAPI spec generation of input and output payloads. My goal is to accept an arbitrary object as input. The way I've found that I can accomplish this is with `z.record(z.any())` and that works just fine functionality wise. The issue I run into is that when I generate the docs, I get the following YAML in the OpenAPI spec that's generated:

```yaml
type: object
additionalProperties:
  format: any
  nullable: true
```

When I check the validity of this using a tool like Redocly's CLI, I get the following error:

```
The `type` field must be defined when the `nullable` field is used.

687 |   additionalProperties:
688 |     format: any
689 |     nullable: true
```

From looking at the OpenAPI spec, this is an accurate error. I was wondering if there was a known workaround/better way to handle a schema field for accepting an arbitrary object. Just using `z.any()` results in the same error as above.

If I manually edit the spec and add `type: object` to the `additionalProperties`, the spec stops throwing that error. Here's an example:

```
type: object
additionalProperties:
  format: any
  nullable: true
  type: object
```

I'm wondering if this is an issue with spec generation, or if I'm just trying to tackle this problem the wrong way. If you have any advice on how I should be using Zod to accept an arbitrary object that passes the OpenAPI spec, I'd love to hear.

Thank you!

Pull request:
Cherry-pick for issue #900 in v9
Same as #908 but for v9,
Fixes #900 

Patch:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 776661fa..aa440853 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -13,10 +13,10 @@ name: "CodeQL"
 
 on:
   push:
-    branches: [ master, v8 ]
+    branches: [ master, v8, v9 ]
   pull_request:
     # The branches below must be a subset of the branches above
-    branches: [ master, v8 ]
+    branches: [ master, v8, v9 ]
   schedule:
     - cron: '26 8 * * 1'
 
diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml
index 671bf19f..a8beb437 100644
--- a/.github/workflows/node.js.yml
+++ b/.github/workflows/node.js.yml
@@ -5,9 +5,9 @@ name: Node.js CI
 
 on:
   push:
-    branches: [ master, v8 ]
+    branches: [ master, v8, v9 ]
   pull_request:
-    branches: [ master, v8 ]
+    branches: [ master, v8, v9 ]
 
 jobs:
   build:
diff --git a/.github/workflows/swagger.yml b/.github/workflows/swagger.yml
index 9ca02155..60b179dd 100644
--- a/.github/workflows/swagger.yml
+++ b/.github/workflows/swagger.yml
@@ -2,9 +2,9 @@ name: OpenAPI Validation
 
 on:
   push:
-    branches: [ master, v8 ]
+    branches: [ master, v8, v9 ]
   pull_request:
-    branches: [ master, v8 ]
+    branches: [ master, v8, v9 ]
 
 
 jobs:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 76fb676e..ffa0411e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,22 @@
 
 ## Version 9
 
+### v9.4.3
+
+- This version contains a cherry-picked fix made in v10.1.1
+- Fixed issue #900, found and reported by [Max Cohn](https://github.com/maxcohn).
+  - Do not set `nullable` property to the depictions having no `type` property according to OpenAPI specification.
+  - Affected schemas: `z.any()` and `z.preprocess()`.
+
+```yaml
+# depiction of z.any() in the generated documentation
+before:
+  format: any
+  nullable: true
+after:
+  format: any
+```
+
 ### v9.4.2
 
 - Fixed issue #892, found and reported by [@McMerph](https://github.com/McMerph).
diff --git a/example/example.swagger.yaml b/example/example.swagger.yaml
index 25ca189e..a9d70042 100644
--- a/example/example.swagger.yaml
+++ b/example/example.swagger.yaml
@@ -306,7 +306,6 @@ paths:
                         type: object
                         additionalProperties:
                           format: any
-                          nullable: true
                     required:
                       - name
                       - size
diff --git a/src/open-api-helpers.ts b/src/open-api-helpers.ts
index 2e6a978a..39b87446 100644
--- a/src/open-api-helpers.ts
+++ b/src/open-api-helpers.ts
@@ -691,18 +691,27 @@ export const depicters: HandlingRules<
   ZodLazy: depictLazy,
 };
 
-export const onEach: Depicter<z.ZodTypeAny, "last"> = ({
+export const onEach: Depicter<z.ZodTypeAny, "each"> = ({
   schema,
   isResponse,
+  prev,
 }) => {
+  if (isReferenceObject(prev)) {
+    return {};
+  }
   const { description } = schema;
   const shouldAvoidParsing = schema instanceof z.ZodLazy;
+  const hasTypePropertyInDepiction = prev.type !== undefined;
+  const isResponseHavingCoercion = isResponse && hasCoercion(schema);
+  const isActuallyNullable =
+    !shouldAvoidParsing &&
+    hasTypePropertyInDepiction &&
+    !isResponseHavingCoercion &&
+    schema.isNullable();
   const examples = shouldAvoidParsing ? [] : getExamples(schema, isResponse);
   return {
     ...(description && { description }),
-    ...(!shouldAvoidParsing &&
-      schema.isNullable() &&
-      !(isResponse && hasCoercion(schema)) && { nullable: true }),
+    ...(isActuallyNullable && { nullable: true }),
     ...(examples.length > 0 && { example: examples[0] }),
   };
 };
diff --git a/src/schema-walker.ts b/src/schema-walker.ts
index a020f8ac..0d2cab99 100644
--- a/src/schema-walker.ts
+++ b/src/schema-walker.ts
@@ -4,14 +4,15 @@ import { ZodDateOutDef } from "./date-out-schema";
 import { ZodFileDef } from "./file-schema";
 import { ZodUploadDef } from "./upload-schema";
 
-export type HandlingVariant = "last" | "regular";
+export type HandlingVariant = "last" | "regular" | "each";
+
 type VariantDependingProps<
   Variant extends HandlingVariant,
   U
 > = Variant extends "regular"
-  ? {
-      next: SchemaHandler<z.ZodTypeAny, U, {}, "last">;
-    }
+  ? { next: SchemaHandler<z.ZodTypeAny, U, {}, "last"> }
+  : Variant extends "each"
+  ? { prev: U }
   : {};
 
 type SchemaHandlingProps<
@@ -51,11 +52,11 @@ export const walkSchema = <U, Context extends object = {}>({
   onMissing,
   ...context
 }: SchemaHandlingProps<z.ZodTypeAny, U, Context, "last"> & {
-  onEach?: SchemaHandler<z.ZodTypeAny, U, Context, "last">;
+  /** @since 10.1.1 calling onEach _after_ handler and giving it its result */
+  onEach?: SchemaHandler<z.ZodTypeAny, U, Context, "each">;
   rules: HandlingRules<U, Context>;
   onMissing: (schema: z.ZodTypeAny) => U;
 }): U => {
-  const overrides = onEach && onEach({ schema, ...(context as Context) });
   const handler =
     "typeName" in schema._def
       ? rules[schema._def.typeName as keyof typeof rules]
@@ -75,5 +76,7 @@ export const walkSchema = <U, Context extends object = {}>({
         next,
       })
     : onMissing(schema);
+  const overrides =
+    onEach && onEach({ schema, prev: result, ...(context as Context) });
   return overrides ? { ...result, ...overrides } : result;
 };


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 fd46cd11..ecfec8ca 100644
--- a/tests/unit/__snapshots__/open-api-helpers.spec.ts.snap
+++ b/tests/unit/__snapshots__/open-api-helpers.spec.ts.snap
@@ -89,7 +89,6 @@ exports[`Open API helpers depictDiscriminatedUnion() should wrap next depicters
       "properties": {
         "data": {
           "format": "any",
-          "nullable": true,
         },
         "status": {
           "enum": [
@@ -168,14 +167,12 @@ exports[`Open API helpers depictEffect() should depict as string (preprocess) 1`
 exports[`Open API helpers depictEffect() should handle edge cases 1`] = `
 {
   "format": "any",
-  "nullable": true,
 }
 `;
 
 exports[`Open API helpers depictEffect() should handle edge cases 2`] = `
 {
   "format": "any",
-  "nullable": true,
 }
 `;
 
@@ -593,7 +590,7 @@ exports[`Open API helpers depictPipeline should depict as string (in) 1`] = `
 }
 `;
 
-exports[`Open API helpers depictRecord() should set properties+required or additionalProperties props 1`] = `
+exports[`Open API helpers depictRecord() should set properties+required or additionalProperties props 0 1`] = `
 {
   "additionalProperties": {
     "type": "boolean",
@@ -602,7 +599,7 @@ exports[`Open API helpers depictRecord() should set properties+required or addit
 }
 `;
 
-exports[`Open API helpers depictRecord() should set properties+required or additionalProperties props 2`] = `
+exports[`Open API helpers depictRecord() should set properties+required or additionalProperties props 1 1`] = `
 {
   "additionalProperties": {
     "type": "boolean",
@@ -611,7 +608,7 @@ exports[`Open API helpers depictRecord() should set properties+required or addit
 }
 `;
 
-exports[`Open API helpers depictRecord() should set properties+required or additionalProperties props 3`] = `
+exports[`Open API helpers depictRecord() should set properties+required or additionalProperties props 2 1`] = `
 {
   "properties": {
     "one": {
@@ -629,7 +626,7 @@ exports[`Open API helpers depictRecord() should set properties+required or addit
 }
 `;
 
-exports[`Open API helpers depictRecord() should set properties+required or additionalProperties props 4`] = `
+exports[`Open API helpers depictRecord() should set properties+required or additionalProperties props 3 1`] = `
 {
   "properties": {
     "testing": {
@@ -643,7 +640,7 @@ exports[`Open API helpers depictRecord() should set properties+required or addit
 }
 `;
 
-exports[`Open API helpers depictRecord() should set properties+required or additionalProperties props 5`] = `
+exports[`Open API helpers depictRecord() should set properties+required or additionalProperties props 4 1`] = `
 {
   "properties": {
     "one": {
@@ -661,6 +658,15 @@ exports[`Open API helpers depictRecord() should set properties+required or addit
 }
 `;
 
+exports[`Open API helpers depictRecord() should set properties+required or additionalProperties props 5 1`] = `
+{
+  "additionalProperties": {
+    "format": "any",
+  },
+  "type": "object",
+}
+`;
+
 exports[`Open API helpers depictRequestParams() should depict none if both query and params are disabled 1`] = `[]`;
 
 exports[`Open API helpers depictRequestParams() should depict only path params if query is disabled 1`] = `
diff --git a/tests/unit/__snapshots__/open-api.spec.ts.snap b/tests/unit/__snapshots__/open-api.spec.ts.snap
index 581795bc..b3b4bb20 100644
--- a/tests/unit/__snapshots__/open-api.spec.ts.snap
+++ b/tests/unit/__snapshots__/open-api.spec.ts.snap
@@ -1507,7 +1507,6 @@ paths:
                               - success
                           data:
                             format: any
-                            nullable: true
                         required:
                           - status
                       - type: object
@@ -2254,7 +2253,6 @@ paths:
                         type: object
                         additionalProperties:
                           format: any
-                          nullable: true
                     required:
                       - name
                       - size
@@ -2720,7 +2718,6 @@ components:
               type: object
               additionalProperties:
                 format: any
-                nullable: true
           required:
             - name
             - size
@@ -3810,7 +3807,6 @@ paths:
                     properties:
                       any:
                         format: any
-                        nullable: true
                 required:
                   - status
                   - data
@@ -3848,7 +3844,6 @@ paths:
           description: GET /v1/getSomething parameter
           schema:
             format: any
-            nullable: true
 components:
   schemas: {}
   responses: {}
@@ -3929,7 +3924,6 @@ paths:
           description: GET /v1/getSomething parameter
           schema:
             format: string (preprocessed)
-            nullable: true
         - name: number
           in: query
           required: true
diff --git a/tests/unit/open-api-helpers.spec.ts b/tests/unit/open-api-helpers.spec.ts
index 5b5d13c0..04195ab0 100644
--- a/tests/unit/open-api-helpers.spec.ts
+++ b/tests/unit/open-api-helpers.spec.ts
@@ -500,8 +500,9 @@ describe("Open API helpers", () => {
       z.record(z.enum(["one", "two"]), z.boolean()),
       z.record(z.literal("testing"), z.boolean()),
       z.record(z.literal("one").or(z.literal("two")), z.boolean()),
+      z.record(z.any()), // Issue #900
     ])(
-      "should set properties+required or additionalProperties props",
+      "should set properties+required or additionalProperties props %#",
       (schema) => {
         expect(
           depictRecord({