Repository: RobinTail/express-zod-api
Pull request #2583: Restoring immutable depicters
Author: login: RobinTail
Labels: name: documentation
description: Improvements or additions to documentation
name: enhancement
description: New feature or request
name: refactoring
description: The better way to achieve the same result
Created: 2025-04-27T05:55:21Z | Merged: 2025-04-27T06:56:28Z

Delegating prop replacement to the framework, so that public interface for custom brands handling could remain similar and more convenient.


Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5c04cf92fa..2a74aa20eb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,7 +15,7 @@
   - Express Zod API implements some overrides and improvements to fit it into OpenAPI 3.1 that extends JSON Schema;
   - The `numericRange` option removed from `Documentation` class constructor argument;
   - The `brandHandling` should consist of postprocessing functions altering the depiction made by Zod 4;
-  - The `Depicter` type changed to `Overrider` having different signature;
+  - The `Depicter` type signature changed;
 - The `optionalPropStyle` option removed from `Integration` class constructor:
   - Use the new `z.interface()` schema to describe key-optional objects: https://v4.zod.dev/v4#zinterface;
 - Changes to the plugin:
diff --git a/README.md b/README.md
index 22308a68c6..6d5541949e 100644
--- a/README.md
+++ b/README.md
@@ -1387,7 +1387,7 @@ const routing: Routing = {
 You can customize handling rules for your schemas in Documentation and Integration. Use the `.brand()` method on your
 schema to make it special and distinguishable for the framework in runtime. Using symbols is recommended for branding.
 After that utilize the `brandHandling` feature of both constructors to declare your custom implementation. In case you
-need to reuse a handling rule for multiple brands, use the exposed types `Overrider` and `Producer`.
+need to reuse a handling rule for multiple brands, use the exposed types `Depicter` and `Producer`.
 
 ```ts
 import ts from "typescript";
@@ -1395,7 +1395,7 @@ import { z } from "zod";
 import {
   Documentation,
   Integration,
-  Overrider,
+  Depicter,
   Producer,
 } from "express-zod-api";
 
@@ -1403,12 +1403,12 @@ const myBrand = Symbol("MamaToldMeImSpecial"); // I recommend to use symbols for
 const myBrandedSchema = z.string().brand(myBrand);
 
 const ruleForDocs: Overrider = (
-  { zodSchema, jsonSchema }, // adjust jsonSchema for overrides
-  { path, method, isResponse }, // handle a nested schema using next()
-) => {
-  delete jsonSchema.format;
-  jsonSchema.summary = "Special type of data";
-};
+  { zodSchema, jsonSchema }, // return changed jsonSchema
+  { path, method, isResponse },
+) => ({
+  ...jsonSchema,
+  summary: "Special type of data",
+});
 
 const ruleForClient: Producer = (
   schema: typeof myBrandedSchema, // you should assign type yourself
diff --git a/express-zod-api/src/documentation-helpers.ts b/express-zod-api/src/documentation-helpers.ts
index 150b55bd8c..3ec7019d1c 100644
--- a/express-zod-api/src/documentation-helpers.ts
+++ b/express-zod-api/src/documentation-helpers.ts
@@ -65,10 +65,10 @@ export interface OpenAPIContext {
   method: Method;
 }
 
-export type Overrider = (
+export type Depicter = (
   zodCtx: { zodSchema: $ZodType; jsonSchema: JSONSchema.BaseSchema },
   oasCtx: OpenAPIContext,
-) => void;
+) => JSONSchema.BaseSchema | SchemaObject;
 
 /** @desc Using defaultIsHeader when returns null or undefined */
 export type IsHeader = (
@@ -77,7 +77,7 @@ export type IsHeader = (
   path: string,
 ) => boolean | null | undefined;
 
-export type BrandHandling = Record<string | symbol, Overrider>;
+export type BrandHandling = Record<string | symbol, Depicter>;
 
 interface ReqResHandlingProps<S extends $ZodType>
   extends Omit<OpenAPIContext, "isResponse"> {
@@ -104,34 +104,36 @@ const samples = {
 export const reformatParamsInPath = (path: string) =>
   path.replace(routePathParamsRegex, (param) => `{${param.slice(1)}}`);
 
-export const onDefault: Overrider = ({ zodSchema, jsonSchema }) =>
-  (jsonSchema.default =
+export const onDefault: Depicter = ({ zodSchema, jsonSchema }) => ({
+  ...jsonSchema,
+  default:
     globalRegistry.get(zodSchema)?.[metaSymbol]?.defaultLabel ??
-    jsonSchema.default);
+    jsonSchema.default,
+});
 
-export const onUpload: Overrider = ({ jsonSchema }, ctx) => {
+export const onUpload: Depicter = ({}, ctx) => {
   if (ctx.isResponse)
     throw new DocumentationError("Please use ez.upload() only for input.", ctx);
-  Object.assign(jsonSchema, { type: "string", format: "binary" });
+  return { type: "string", format: "binary" };
 };
 
-export const onFile: Overrider = ({ jsonSchema }) => {
-  delete jsonSchema.anyOf; // undo default
-  Object.assign(jsonSchema, {
-    type: "string",
-    format:
-      jsonSchema.type === "string"
-        ? jsonSchema.format === "base64"
-          ? "byte"
-          : "file"
-        : "binary",
-  });
-};
+export const onFile: Depicter = ({ jsonSchema }) => ({
+  type: "string",
+  format:
+    jsonSchema.type === "string"
+      ? jsonSchema.format === "base64"
+        ? "byte"
+        : "file"
+      : "binary",
+});
 
-export const onUnion: Overrider = ({ zodSchema, jsonSchema }) => {
-  if (!zodSchema._zod.disc) return;
+export const onUnion: Depicter = ({ zodSchema, jsonSchema }) => {
+  if (!zodSchema._zod.disc) return jsonSchema;
   const propertyName = Array.from(zodSchema._zod.disc.keys()).pop();
-  jsonSchema.discriminator ??= { propertyName };
+  return {
+    ...jsonSchema,
+    discriminator: jsonSchema.discriminator ?? { propertyName },
+  };
 };
 
 const propsMerger = (a: unknown, b: unknown) => {
@@ -180,22 +182,20 @@ const intersect = (
   return R.map((fn) => fn(left, right), suitable);
 };
 
-export const onIntersection: Overrider = ({ jsonSchema }) => {
-  if (!jsonSchema.allOf) return;
-  try {
-    const attempt = intersect(jsonSchema.allOf);
-    delete jsonSchema.allOf; // undo default
-    Object.assign(jsonSchema, attempt);
-  } catch {}
+export const onIntersection: Depicter = ({ jsonSchema }) => {
+  if (jsonSchema.allOf) {
+    try {
+      return intersect(jsonSchema.allOf);
+    } catch {}
+  }
+  return jsonSchema;
 };
 
 /** @since OAS 3.1 nullable replaced with type array having null */
-export const onNullable: Overrider = ({ jsonSchema }) => {
-  if (!jsonSchema.anyOf) return;
+export const onNullable: Depicter = ({ jsonSchema }) => {
+  if (!jsonSchema.anyOf) return jsonSchema;
   const original = jsonSchema.anyOf[0];
-  Object.assign(original, { type: makeNullableType(original.type) });
-  Object.assign(jsonSchema, original);
-  delete jsonSchema.anyOf;
+  return Object.assign(original, { type: makeNullableType(original.type) });
 };
 
 const isSupportedType = (subject: string): subject is SchemaObjectType =>
@@ -226,11 +226,10 @@ const ensureCompliance = ({
   return valid;
 };
 
-export const onDateIn: Overrider = ({ jsonSchema }, ctx) => {
+export const onDateIn: Depicter = ({}, ctx) => {
   if (ctx.isResponse)
     throw new DocumentationError("Please use ez.dateOut() for output.", ctx);
-  delete jsonSchema.anyOf; // undo default
-  Object.assign(jsonSchema, {
+  return {
     description: "YYYY-MM-DDTHH:mm:ss.sssZ",
     type: "string",
     format: "date-time",
@@ -238,37 +237,36 @@ export const onDateIn: Overrider = ({ jsonSchema }, ctx) => {
     externalDocs: {
       url: isoDateDocumentationUrl,
     },
-  });
+  };
 };
 
-export const onDateOut: Overrider = ({ jsonSchema }, ctx) => {
+export const onDateOut: Depicter = ({}, ctx) => {
   if (!ctx.isResponse)
     throw new DocumentationError("Please use ez.dateIn() for input.", ctx);
-  Object.assign(jsonSchema, {
+  return {
     description: "YYYY-MM-DDTHH:mm:ss.sssZ",
     type: "string",
     format: "date-time",
     externalDocs: {
       url: isoDateDocumentationUrl,
     },
-  });
+  };
 };
 
-export const onBigInt: Overrider = ({ jsonSchema }) =>
-  Object.assign(jsonSchema, {
-    type: "string",
-    format: "bigint",
-    pattern: /^-?\d+$/.source,
-  });
+export const onBigInt: Depicter = () => ({
+  type: "string",
+  format: "bigint",
+  pattern: /^-?\d+$/.source,
+});
 
 /**
  * @since OAS 3.1 using prefixItems for depicting tuples
  * @since 17.5.0 added rest handling, fixed tuple type
  */
-export const onTuple: Overrider = ({ zodSchema, jsonSchema }) => {
-  if ((zodSchema as $ZodTuple)._zod.def.rest !== null) return;
+export const onTuple: Depicter = ({ zodSchema, jsonSchema }) => {
+  if ((zodSchema as $ZodTuple)._zod.def.rest !== null) return jsonSchema;
   // does not appear to support items:false, so not:{} is a recommended alias
-  jsonSchema.items = { not: {} };
+  return { ...jsonSchema, items: { not: {} } };
 };
 
 const makeSample = (depicted: SchemaObject) => {
@@ -292,44 +290,43 @@ const makeNullableType = (
   );
 };
 
-export const onPipeline: Overrider = ({ zodSchema, jsonSchema }, ctx) => {
+export const onPipeline: Depicter = ({ zodSchema, jsonSchema }, ctx) => {
   const target = (zodSchema as $ZodPipe)._zod.def[
     ctx.isResponse ? "out" : "in"
   ];
   const opposite = (zodSchema as $ZodPipe)._zod.def[
     ctx.isResponse ? "in" : "out"
   ];
-  if (!isSchema<$ZodTransform>(target, "transform")) return;
+  if (!isSchema<$ZodTransform>(target, "transform")) return jsonSchema;
   const opposingDepiction = depict(opposite, { ctx });
   if (isSchemaObject(opposingDepiction)) {
     if (!ctx.isResponse) {
       const { type: opposingType, ...rest } = opposingDepiction;
-      Object.assign(jsonSchema, {
+      return {
         ...rest,
         format: `${rest.format || opposingType} (preprocessed)`,
-      });
+      };
     } else {
       const targetType = getTransformedType(
         target,
         makeSample(opposingDepiction),
       );
       if (targetType && ["number", "string", "boolean"].includes(targetType)) {
-        Object.assign(jsonSchema, {
+        return {
           type: targetType as "number" | "string" | "boolean",
-        });
+        };
       }
     }
   }
+  return jsonSchema;
 };
 
-export const onRaw: Overrider = ({ jsonSchema }) => {
-  if (jsonSchema.type !== "object") return;
+export const onRaw: Depicter = ({ jsonSchema }) => {
+  if (jsonSchema.type !== "object") return jsonSchema;
   const objSchema = jsonSchema as JSONSchema.ObjectSchema;
-  if (!objSchema.properties) return;
-  if (!("raw" in objSchema.properties)) return;
-  Object.assign(jsonSchema, objSchema.properties.raw);
-  delete jsonSchema.properties; // undo default
-  delete jsonSchema.required;
+  if (!objSchema.properties) return jsonSchema;
+  if (!("raw" in objSchema.properties)) return jsonSchema;
+  return objSchema.properties.raw;
 };
 
 const enumerateExamples = (examples: unknown[]): ExamplesObject | undefined =>
@@ -425,7 +422,7 @@ export const depictRequestParams = ({
             : undefined;
       if (!location) return acc;
       const depicted = depict(paramSchema, {
-        rules: { ...brandHandling, ...overrides },
+        rules: { ...brandHandling, ...depicters },
         ctx: { isResponse: false, makeRef, path, method },
       });
       const result =
@@ -446,7 +443,7 @@ export const depictRequestParams = ({
   );
 };
 
-const overrides: Partial<Record<FirstPartyKind | ProprietaryBrand, Overrider>> =
+const depicters: Partial<Record<FirstPartyKind | ProprietaryBrand, Depicter>> =
   {
     nullable: onNullable,
     default: onDefault,
@@ -462,15 +459,17 @@ const overrides: Partial<Record<FirstPartyKind | ProprietaryBrand, Overrider>> =
     [ezRawBrand]: onRaw,
   };
 
-const onEach: Overrider = ({ zodSchema, jsonSchema }, { isResponse }) => {
+const onEach: Depicter = ({ zodSchema, jsonSchema }, { isResponse }) => {
+  const result = { ...jsonSchema };
   if (!isResponse && doesAccept(zodSchema, null))
-    Object.assign(jsonSchema, { type: makeNullableType(jsonSchema.type) });
+    Object.assign(result, { type: makeNullableType(jsonSchema.type) });
   const examples = getExamples({
     schema: zodSchema,
     variant: isResponse ? "parsed" : "original",
     validate: true,
   });
-  if (examples.length) jsonSchema.examples = examples.slice();
+  if (examples.length) result.examples = examples.slice();
+  return result;
 };
 
 /**
@@ -514,7 +513,7 @@ const unref = (
 
 const depict = (
   subject: $ZodType,
-  { ctx, rules = overrides }: { ctx: OpenAPIContext; rules?: BrandHandling },
+  { ctx, rules = depicters }: { ctx: OpenAPIContext; rules?: BrandHandling },
 ) => {
   const { $defs = {}, properties = {} } = z.toJSONSchema(
     z.object({ subject }), // avoiding "document root" references
@@ -525,10 +524,16 @@ const depict = (
         unref(zodCtx.jsonSchema);
         const { brand } =
           globalRegistry.get(zodCtx.zodSchema)?.[metaSymbol] ?? {};
-        rules[
-          brand && brand in rules ? brand : zodCtx.zodSchema._zod.def.type
-        ]?.(zodCtx, ctx);
-        onEach(zodCtx, ctx);
+        const depicter =
+          rules[
+            brand && brand in rules ? brand : zodCtx.zodSchema._zod.def.type
+          ];
+        if (depicter) {
+          const overrides = { ...depicter(zodCtx, ctx) };
+          for (const key in zodCtx.jsonSchema) delete zodCtx.jsonSchema[key];
+          Object.assign(zodCtx.jsonSchema, overrides);
+        }
+        Object.assign(zodCtx.jsonSchema, onEach(zodCtx, ctx));
       },
     },
   ) as JSONSchema.ObjectSchema;
@@ -587,7 +592,7 @@ export const depictResponse = ({
   if (!mimeTypes) return { description };
   const depictedSchema = excludeExamplesFromDepiction(
     depict(schema, {
-      rules: { ...brandHandling, ...overrides },
+      rules: { ...brandHandling, ...depicters },
       ctx: { isResponse: true, makeRef, path, method },
     }),
   );
@@ -709,7 +714,7 @@ export const depictBody = ({
 }) => {
   const [withoutParams, hasRequired] = excludeParamsFromDepiction(
     depict(schema, {
-      rules: { ...brandHandling, ...overrides },
+      rules: { ...brandHandling, ...depicters },
       ctx: { isResponse: false, makeRef, path, method },
     }),
     paramNames,
diff --git a/express-zod-api/src/index.ts b/express-zod-api/src/index.ts
index d76fc50361..76db034948 100644
--- a/express-zod-api/src/index.ts
+++ b/express-zod-api/src/index.ts
@@ -33,7 +33,7 @@ export { EventStreamFactory } from "./sse";
 export { ez } from "./proprietary-schemas";
 
 // Convenience types
-export type { Overrider } from "./documentation-helpers";
+export type { Depicter } from "./documentation-helpers";
 export type { Producer } from "./zts-helpers";
 
 // Interfaces exposed for augmentation
diff --git a/express-zod-api/tests/documentation-helpers.spec.ts b/express-zod-api/tests/documentation-helpers.spec.ts
index 1f42c0c63f..2127726e82 100644
--- a/express-zod-api/tests/documentation-helpers.spec.ts
+++ b/express-zod-api/tests/documentation-helpers.spec.ts
@@ -127,8 +127,9 @@ describe("Documentation helpers", () => {
         default: "2025-05-21",
         format: "date-time",
       };
-      onDefault({ zodSchema, jsonSchema }, responseCtx);
-      expect(jsonSchema).toMatchSnapshot();
+      expect(
+        onDefault({ zodSchema, jsonSchema }, responseCtx),
+      ).toMatchSnapshot();
     });
   });
 
@@ -138,21 +139,21 @@ describe("Documentation helpers", () => {
         type: "object",
         properties: { raw: { format: "binary", type: "string" } },
       };
-      onRaw({ zodSchema: z.never(), jsonSchema }, requestCtx);
-      expect(jsonSchema).toMatchSnapshot();
+      expect(
+        onRaw({ zodSchema: z.never(), jsonSchema }, requestCtx),
+      ).toMatchSnapshot();
     });
   });
 
   describe("onUpload()", () => {
-    const jsonSchema: JSONSchema.BaseSchema = {};
-    const zodSchema = z.never();
     test("should set format:binary and type:string", () => {
-      onUpload({ zodSchema, jsonSchema }, requestCtx);
-      expect(jsonSchema).toMatchSnapshot();
+      expect(
+        onUpload({ zodSchema: z.never(), jsonSchema: {} }, requestCtx),
+      ).toMatchSnapshot();
     });
     test("should throw when using in response", () => {
       expect(() =>
-        onUpload({ zodSchema, jsonSchema }, responseCtx),
+        onUpload({ zodSchema: z.never(), jsonSchema: {} }, responseCtx),
       ).toThrowErrorMatchingSnapshot();
     });
   });
@@ -165,8 +166,9 @@ describe("Documentation helpers", () => {
       { anyOf: [], type: "string" },
       {},
     ])("should set type:string and format accordingly %#", (jsonSchema) => {
-      onFile({ zodSchema: z.never(), jsonSchema }, responseCtx);
-      expect(jsonSchema).toMatchSnapshot();
+      expect(
+        onFile({ zodSchema: z.never(), jsonSchema }, responseCtx),
+      ).toMatchSnapshot();
     });
   });
 
@@ -179,9 +181,9 @@ describe("Documentation helpers", () => {
           error: z.object({ message: z.string() }),
         }),
       ]);
-      const jsonSchema: JSONSchema.BaseSchema = {};
-      onUnion({ zodSchema, jsonSchema }, requestCtx);
-      expect(jsonSchema).toMatchSnapshot();
+      expect(
+        onUnion({ zodSchema, jsonSchema: {} }, requestCtx),
+      ).toMatchSnapshot();
     });
   });
 
@@ -197,8 +199,9 @@ describe("Documentation helpers", () => {
           { type: "object", properties: { two: { type: "number" } } },
         ],
       };
-      onIntersection({ zodSchema: z.never(), jsonSchema }, requestCtx);
-      expect(jsonSchema).toMatchSnapshot();
+      expect(
+        onIntersection({ zodSchema: z.never(), jsonSchema }, requestCtx),
+      ).toMatchSnapshot();
     });
 
     test("should flatten objects with same prop of same type", () => {
@@ -208,8 +211,9 @@ describe("Documentation helpers", () => {
           { type: "object", properties: { one: { type: "number" } } },
         ],
       };
-      onIntersection({ zodSchema: z.never(), jsonSchema }, requestCtx);
-      expect(jsonSchema).toMatchSnapshot();
+      expect(
+        onIntersection({ zodSchema: z.never(), jsonSchema }, requestCtx),
+      ).toMatchSnapshot();
     });
 
     test("should NOT flatten object schemas having conflicting props", () => {
@@ -219,8 +223,9 @@ describe("Documentation helpers", () => {
           { type: "object", properties: { one: { type: "string" } } },
         ],
       };
-      onIntersection({ zodSchema: z.never(), jsonSchema }, requestCtx);
-      expect(jsonSchema).toMatchSnapshot();
+      expect(
+        onIntersection({ zodSchema: z.never(), jsonSchema }, requestCtx),
+      ).toMatchSnapshot();
     });
 
     test("should merge examples deeply", () => {
@@ -238,8 +243,9 @@ describe("Documentation helpers", () => {
           },
         ],
       };
-      onIntersection({ zodSchema: z.never(), jsonSchema }, requestCtx);
-      expect(jsonSchema).toMatchSnapshot();
+      expect(
+        onIntersection({ zodSchema: z.never(), jsonSchema }, requestCtx),
+      ).toMatchSnapshot();
     });
 
     test("should maintain uniqueness in the array of required props", () => {
@@ -249,8 +255,9 @@ describe("Documentation helpers", () => {
           { type: "object", properties: { test: { const: 5 } } },
         ],
       };
-      onIntersection({ zodSchema: z.never(), jsonSchema }, requestCtx);
-      expect(jsonSchema).toMatchSnapshot();
+      expect(
+        onIntersection({ zodSchema: z.never(), jsonSchema }, requestCtx),
+      ).toMatchSnapshot();
     });
 
     test.each<JSONSchema.BaseSchema>([
@@ -272,8 +279,9 @@ describe("Documentation helpers", () => {
         allOf: [{ type: "number" }, { const: 5 }], // not objects
       },
     ])("should fall back to allOf in other cases %#", (jsonSchema) => {
-      onIntersection({ zodSchema: z.never(), jsonSchema }, requestCtx);
-      expect(jsonSchema).toHaveProperty("allOf");
+      expect(
+        onIntersection({ zodSchema: z.never(), jsonSchema }, requestCtx),
+      ).toHaveProperty("allOf");
     });
   });
 
@@ -284,8 +292,9 @@ describe("Documentation helpers", () => {
         const jsonSchema: JSONSchema.BaseSchema = {
           anyOf: [{ type: "string" }, { type: "null" }],
         };
-        onNullable({ zodSchema: z.never(), jsonSchema }, ctx);
-        expect(jsonSchema).toMatchSnapshot();
+        expect(
+          onNullable({ zodSchema: z.never(), jsonSchema }, ctx),
+        ).toMatchSnapshot();
       },
     );
 
@@ -293,16 +302,17 @@ describe("Documentation helpers", () => {
       const jsonSchema: JSONSchema.BaseSchema = {
         anyOf: [{ type: "null" }, { type: "null" }],
       };
-      onNullable({ zodSchema: z.never(), jsonSchema }, requestCtx);
-      expect(jsonSchema).toMatchSnapshot();
+      expect(
+        onNullable({ zodSchema: z.never(), jsonSchema }, requestCtx),
+      ).toMatchSnapshot();
     });
   });
 
   describe("onBigInt()", () => {
     test("should set type:string and format:bigint", () => {
-      const jsonSchema: JSONSchema.BaseSchema = {};
-      onBigInt({ zodSchema: z.never(), jsonSchema }, requestCtx);
-      expect(jsonSchema).toMatchSnapshot();
+      expect(
+        onBigInt({ zodSchema: z.never(), jsonSchema: {} }, requestCtx),
+      ).toMatchSnapshot();
     });
   });
 
@@ -311,9 +321,9 @@ describe("Documentation helpers", () => {
       z.tuple([z.boolean(), z.string(), z.literal("test")]),
       z.tuple([]),
     ])("should add items:not:{} when no rest %#", (zodSchema) => {
-      const jsonSchema: JSONSchema.BaseSchema = {};
-      onTuple({ zodSchema, jsonSchema }, requestCtx);
-      expect(jsonSchema).toMatchSnapshot();
+      expect(
+        onTuple({ zodSchema, jsonSchema: {} }, requestCtx),
+      ).toMatchSnapshot();
     });
   });
 
@@ -330,18 +340,16 @@ describe("Documentation helpers", () => {
         expected: "string (preprocess)",
       },
     ])("should depict as $expected", ({ zodSchema, ctx }) => {
-      const jsonSchema: JSONSchema.BaseSchema = {};
-      onPipeline({ zodSchema, jsonSchema }, ctx);
-      expect(jsonSchema).toMatchSnapshot();
+      expect(onPipeline({ zodSchema, jsonSchema: {} }, ctx)).toMatchSnapshot();
     });
 
     test.each([
       z.number().transform((num) => () => num),
       z.number().transform(() => assert.fail("this should be handled")),
     ])("should handle edge cases %#", (zodSchema) => {
-      const jsonSchema: JSONSchema.BaseSchema = {};
-      onPipeline({ zodSchema, jsonSchema }, responseCtx);
-      expect(jsonSchema).toEqual({});
+      expect(onPipeline({ zodSchema, jsonSchema: {} }, responseCtx)).toEqual(
+        {},
+      );
     });
   });
 
@@ -494,9 +502,12 @@ describe("Documentation helpers", () => {
 
   describe("onDateIn", () => {
     test("should set type:string, pattern and format", () => {
-      const jsonSchema: JSONSchema.BaseSchema = { anyOf: [] };
-      onDateIn({ zodSchema: z.never(), jsonSchema }, requestCtx);
-      expect(jsonSchema).toMatchSnapshot();
+      expect(
+        onDateIn(
+          { zodSchema: z.never(), jsonSchema: { anyOf: [] } },
+          requestCtx,
+        ),
+      ).toMatchSnapshot();
     });
     test("should throw when ZodDateIn in response", () => {
       expect(() =>
@@ -507,9 +518,9 @@ describe("Documentation helpers", () => {
 
   describe("onDateOut", () => {
     test("should set type:string, description and format", () => {
-      const jsonSchema: JSONSchema.BaseSchema = {};
-      onDateOut({ zodSchema: z.never(), jsonSchema }, responseCtx);
-      expect(jsonSchema).toMatchSnapshot();
+      expect(
+        onDateOut({ zodSchema: z.never(), jsonSchema: {} }, responseCtx),
+      ).toMatchSnapshot();
     });
     test("should throw when ZodDateOut in request", () => {
       expect(() =>
diff --git a/express-zod-api/tests/documentation.spec.ts b/express-zod-api/tests/documentation.spec.ts
index 02176b2d2b..37b23a5f1c 100644
--- a/express-zod-api/tests/documentation.spec.ts
+++ b/express-zod-api/tests/documentation.spec.ts
@@ -9,7 +9,7 @@ import {
   defaultEndpointsFactory,
   ez,
   ResultHandler,
-  Overrider,
+  Depicter,
 } from "../src";
 import { contentTypes } from "../src/content-type";
 import { z } from "zod";
@@ -1185,7 +1185,10 @@ describe("Documentation", () => {
   describe("Feature #1470: Custom brands", () => {
     test("should be handled accordingly in request, response and params", () => {
       const deep = Symbol("DEEP");
-      const rule: Overrider = ({ jsonSchema }) => (jsonSchema.type = "boolean");
+      const rule: Depicter = ({ jsonSchema }) => ({
+        ...jsonSchema,
+        type: "boolean",
+      });
       const spec = new Documentation({
         config: sampleConfig,
         routing: {
@@ -1204,7 +1207,10 @@ describe("Documentation", () => {
           },
         },
         brandHandling: {
-          CUSTOM: ({ jsonSchema }) => (jsonSchema.summary = "My custom schema"),
+          CUSTOM: ({ jsonSchema }) => ({
+            ...jsonSchema,
+            summary: "My custom schema",
+          }),
           [deep]: rule,
         },
         version: "3.4.5",
diff --git a/express-zod-api/tests/index.spec.ts b/express-zod-api/tests/index.spec.ts
index 011114f6ce..037a309c5e 100644
--- a/express-zod-api/tests/index.spec.ts
+++ b/express-zod-api/tests/index.spec.ts
@@ -12,7 +12,7 @@ import {
   CommonConfig,
   CookieSecurity,
   HeaderSecurity,
-  Overrider,
+  Depicter,
   FlatObject,
   IOSchema,
   InputSecurity,
@@ -40,8 +40,13 @@ describe("Index Entrypoint", () => {
 
     test("Convenience types should be exposed", () => {
       expectTypeOf(
-        ({}: { zodSchema: $ZodType; jsonSchema: JSONSchema.BaseSchema }) => {},
-      ).toExtend<Overrider>();
+        ({
+          jsonSchema,
+        }: {
+          zodSchema: $ZodType;
+          jsonSchema: JSONSchema.BaseSchema;
+        }) => jsonSchema,
+      ).toExtend<Depicter>();
       expectTypeOf(() =>
         ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword),
       ).toExtend<Producer>();


Reviews:
user: login: RobinTail
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-27T06:54:00Z
user: login: RobinTail
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-27T06:54:27Z
user: login: RobinTail
body: nice
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-27T06:56:20Z

Review comments:
diff_hunk: @@ -1405,10 +1405,10 @@ const myBrandedSchema = z.string().brand(myBrand);
 const ruleForDocs: Overrider = (
   { zodSchema, jsonSchema }, // adjust jsonSchema for overrides
path: README.md
user: login: RobinTail
body: ```suggestion
  { zodSchema, jsonSchema }, // return changed jsonSchema
```
created_at: 2025-04-27T06:54:00Z
author_association: OWNER
diff_hunk: @@ -1387,28 +1387,28 @@ const routing: Routing = {
 You can customize handling rules for your schemas in Documentation and Integration. Use the `.brand()` method on your
 schema to make it special and distinguishable for the framework in runtime. Using symbols is recommended for branding.
 After that utilize the `brandHandling` feature of both constructors to declare your custom implementation. In case you
-need to reuse a handling rule for multiple brands, use the exposed types `Overrider` and `Producer`.
+need to reuse a handling rule for multiple brands, use the exposed types `Depicter` and `Producer`.
 
 ```ts
 import ts from "typescript";
 import { z } from "zod";
 import {
   Documentation,
   Integration,
-  Overrider,
+  Depicter,
   Producer,
 } from "express-zod-api";
 
 const myBrand = Symbol("MamaToldMeImSpecial"); // I recommend to use symbols for this purpose
 const myBrandedSchema = z.string().brand(myBrand);
 
 const ruleForDocs: Overrider = (
-  { zodSchema, jsonSchema }, // adjust jsonSchema for overrides
+  { zodSchema, jsonSchema }, // return changed jsonSchema
   { path, method, isResponse }, // handle a nested schema using next()
path: README.md
user: login: RobinTail
body: ```suggestion
  { path, method, isResponse },
```
created_at: 2025-04-27T06:54:27Z
author_association: OWNER

PR comments:
user: login: RobinTail
created_at: 2025-04-27T06:17:05Z
body: Experiment is a success.
Need to:
- Restore name `Depicter`
- Adjust documentation and changelog
author_association: OWNER
user: login: coveralls-official[bot]
created_at: 2025-04-27T06:17:18Z
body: 
[![Coverage Status](https://coveralls.io/builds/73400241/badge)](https://coveralls.io/builds/73400241)

coverage: 100.0%. remained the same
when pulling **8766ca6e68273d08dbc5052d2b4364c6260df8c4 on back-to-immutable-depicters**
into **3829416fc988c0c238361f387ca8c7cdd1af314f on make-v24**.

author_association: NONE