diff --git a/express-zod-api/src/io-schema.ts b/express-zod-api/src/io-schema.ts
index 7d44e8194..b0cca8fda 100644
--- a/express-zod-api/src/io-schema.ts
+++ b/express-zod-api/src/io-schema.ts
@@ -1,7 +1,7 @@
import * as R from "ramda";
import { z } from "zod";
import { IOSchemaError } from "./errors";
-import { copyMeta } from "./metadata";
+import { mixExamples } from "./metadata";
import { AbstractMiddleware } from "./middleware";
type Base = object & { [Symbol.iterator]?: never };
@@ -14,7 +14,7 @@ export type IOSchema = z.ZodType;
* @since 07.03.2022 former combineEndpointAndMiddlewareInputSchemas()
* @since 05.03.2023 is immutable to metadata
* @since 26.05.2024 uses the regular ZodIntersection
- * @see copyMeta
+ * @see mixExamples
*/
export const getFinalEndpointInputSchema = <
MIN extends IOSchema,
@@ -29,7 +29,7 @@ export const getFinalEndpointInputSchema = <
z.intersection(acc, schema),
);
return allSchemas.reduce(
- (acc, schema) => copyMeta(schema, acc),
+ (acc, schema) => mixExamples(schema, acc),
finalSchema,
) as z.ZodIntersection;
};
diff --git a/express-zod-api/src/metadata.ts b/express-zod-api/src/metadata.ts
index 830d0f1bb..6455382d4 100644
--- a/express-zod-api/src/metadata.ts
+++ b/express-zod-api/src/metadata.ts
@@ -11,26 +11,26 @@ export interface Metadata {
brand?: string | number | symbol;
}
-// @todo this should be renamed to copyExamples or mixinExamples or something similar
-export const copyMeta = (
+export const mixExamples = (
src: A,
dest: B,
): B => {
- const srcMeta = src.meta()?.[metaSymbol];
- const destMeta = dest.meta()?.[metaSymbol];
- if (!srcMeta) return dest; // ensure metadata in src below
+ const srcMeta = src.meta();
+ const destMeta = dest.meta();
+ if (!srcMeta?.[metaSymbol]) return dest; // ensures srcMeta[metaSymbol]
+ const examples = combinations(
+ destMeta?.[metaSymbol]?.examples || [],
+ srcMeta[metaSymbol].examples || [],
+ ([destExample, srcExample]) =>
+ typeof destExample === "object" &&
+ typeof srcExample === "object" &&
+ destExample &&
+ srcExample
+ ? R.mergeDeepRight(destExample, srcExample)
+ : srcExample, // not supposed to be called on non-object schemas
+ );
return dest.meta({
- description: dest.description,
- [metaSymbol]: {
- ...destMeta,
- examples: combinations(
- destMeta?.examples || [],
- srcMeta.examples || [],
- ([destExample, srcExample]) =>
- typeof destExample === "object" && typeof srcExample === "object"
- ? R.mergeDeepRight({ ...destExample }, { ...srcExample })
- : srcExample, // not supposed to be called on non-object schemas
- ),
- },
+ ...destMeta,
+ [metaSymbol]: { ...destMeta?.[metaSymbol], examples },
});
};
diff --git a/express-zod-api/src/zod-plugin.ts b/express-zod-api/src/zod-plugin.ts
index 0526ea467..cdb7b753b 100644
--- a/express-zod-api/src/zod-plugin.ts
+++ b/express-zod-api/src/zod-plugin.ts
@@ -6,6 +6,7 @@
* @desc Enables .label() on ZodDefault
* @desc Enables .remap() on ZodObject
* @desc Stores the argument supplied to .brand() on all schema (runtime distinguishable branded types)
+ * @desc Ensures that the brand withstands additional refinements or checks
* */
import * as R from "ramda";
import { z, globalRegistry } from "zod";
@@ -55,20 +56,19 @@ declare module "zod" {
}
const exampleSetter = function (this: z.ZodType, value: z.input) {
- const { examples, ...rest } = this.meta()?.[metaSymbol] || { examples: [] };
- const copy = examples.slice();
+ const { [metaSymbol]: internal, ...rest } = this.meta() || {};
+ const copy = internal?.examples.slice() || [];
copy.push(value);
return this.meta({
- description: this.description,
- [metaSymbol]: { ...rest, examples: copy },
+ ...rest,
+ [metaSymbol]: { ...internal, examples: copy },
});
};
const deprecationSetter = function (this: z.ZodType) {
return this.meta({
- description: this.description,
+ ...this.meta(),
deprecated: true,
- [metaSymbol]: this.meta()?.[metaSymbol],
});
};
@@ -76,9 +76,11 @@ const labelSetter = function (
this: z.ZodDefault,
defaultLabel: string,
) {
+ const { [metaSymbol]: internal = { examples: [] }, ...rest } =
+ this.meta() || {};
return this.meta({
- description: this.description,
- [metaSymbol]: { examples: [], ...this.meta()?.[metaSymbol], defaultLabel },
+ ...rest,
+ [metaSymbol]: { ...internal, defaultLabel },
});
};
@@ -86,9 +88,11 @@ const brandSetter = function (
this: z.ZodType,
brand?: string | number | symbol,
) {
+ const { [metaSymbol]: internal = { examples: [] }, ...rest } =
+ this.meta() || {};
return this.meta({
- description: this.description,
- [metaSymbol]: { examples: [], ...this.meta()?.[metaSymbol], brand },
+ ...rest,
+ [metaSymbol]: { ...internal, brand },
});
};
diff --git a/express-zod-api/tests/metadata.spec.ts b/express-zod-api/tests/metadata.spec.ts
index 36b32d4c2..40ac7dd5c 100644
--- a/express-zod-api/tests/metadata.spec.ts
+++ b/express-zod-api/tests/metadata.spec.ts
@@ -1,24 +1,26 @@
import { z } from "zod";
-import { copyMeta, metaSymbol } from "../src/metadata";
+import { mixExamples, metaSymbol } from "../src/metadata";
describe("Metadata", () => {
- describe("copyMeta()", () => {
+ describe("mixExamples()", () => {
test("should return the same dest schema in case src one has no meta", () => {
const src = z.string();
const dest = z.number();
- const result = copyMeta(src, dest);
+ const result = mixExamples(src, dest);
expect(result).toEqual(dest);
expect(result.meta()?.[metaSymbol]).toBeFalsy();
expect(dest.meta()?.[metaSymbol]).toBeFalsy();
});
test("should copy meta from src to dest in case meta is defined", () => {
- const src = z.string().example("some");
- const dest = z.number();
- const result = copyMeta(src, dest);
+ const src = z.string().example("some").describe("test");
+ const dest = z.number().describe("another");
+ const result = mixExamples(src, dest);
+ expect(result).not.toEqual(dest); // immutable
expect(result.meta()?.[metaSymbol]).toBeTruthy();
expect(result.meta()?.[metaSymbol]?.examples).toEqual(
src.meta()?.[metaSymbol]?.examples,
);
+ expect(result.description).toBe("another"); // preserves it
});
test("should merge the meta from src to dest", () => {
@@ -31,7 +33,7 @@ describe("Metadata", () => {
.example({ b: 123 })
.example({ b: 456 })
.example({ b: 789 });
- const result = copyMeta(src, dest);
+ const result = mixExamples(src, dest);
expect(result.meta()?.[metaSymbol]).toBeTruthy();
expect(result.meta()?.[metaSymbol]?.examples).toEqual([
{ a: "some", b: 123 },
@@ -53,7 +55,7 @@ describe("Metadata", () => {
.example({ a: { c: 123 } })
.example({ a: { c: 456 } })
.example({ a: { c: 789 } });
- const result = copyMeta(src, dest);
+ const result = mixExamples(src, dest);
expect(result.meta()?.[metaSymbol]).toBeTruthy();
expect(result.meta()?.[metaSymbol]?.examples).toEqual([
{ a: { b: "some", c: 123 } },
@@ -70,7 +72,7 @@ describe("Metadata", () => {
const dest = z
.object({ items: z.array(z.string()) })
.example({ items: ["e", "f", "g"] });
- const result = copyMeta(src, dest);
+ const result = mixExamples(src, dest);
expect(result.meta()?.[metaSymbol]?.examples).toEqual(["a", "b"]);
});
});