Skip to content

Commit 46adaf2

Browse files
authored
Changes to optionality (#2633)
- address replaced `optionaility` with `optin` and `optout` - that also fixed question marks on optional schemas with transformations - colinhacks/zod#4322 - new bug: colinhacks/zod#4407 - also, it appears to change the way `.meta()` works: now it seems to merge the metadata
1 parent 6248795 commit 46adaf2

File tree

10 files changed

+40
-49
lines changed

10 files changed

+40
-49
lines changed

express-zod-api/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@
7676
"express-fileupload": "^1.5.0",
7777
"http-errors": "^2.0.0",
7878
"typescript": "^5.1.3",
79-
"zod": "3.25.0-beta.20250518T002810"
79+
"zod": "3.25.0-beta.20250519T051133"
8080
},
8181
"peerDependenciesMeta": {
8282
"@types/compression": {

express-zod-api/src/common-helpers.ts

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
import type {
2-
$ZodObject,
3-
$ZodTransform,
4-
$ZodType,
5-
$ZodTypeInternals,
6-
} from "zod/v4/core";
1+
import type { $ZodObject, $ZodTransform, $ZodType } from "zod/v4/core";
72
import { Request } from "express";
83
import * as R from "ramda";
94
import { globalRegistry, z } from "zod/v4";
@@ -175,17 +170,10 @@ export const getTransformedType = R.tryCatch(
175170
R.always(undefined),
176171
);
177172

178-
const requestOptionality: Array<$ZodTypeInternals["optionality"]> = [
179-
"optional",
180-
"defaulted",
181-
];
182173
export const isOptional = (
183-
{ _zod: { optionality } }: $ZodType,
174+
{ _zod: { optin, optout } }: $ZodType,
184175
{ isResponse }: { isResponse: boolean },
185-
) =>
186-
isResponse
187-
? optionality === "optional"
188-
: optionality && requestOptionality.includes(optionality);
176+
) => (isResponse ? optout : optin) === "optional";
189177

190178
/** @desc can still be an array, use Array.isArray() or rather R.type() to exclude that case */
191179
export const isObject = (subject: unknown) =>

express-zod-api/src/zod-plugin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ const brandSetter = function (
9393
const { [metaSymbol]: internal = { examples: [] }, ...rest } =
9494
this.meta() || {};
9595
return this.meta({
96-
...rest,
96+
...rest, // @todo this may no longer be required since it seems that .meta() merges now, not just overrides
9797
[metaSymbol]: { ...internal, brand },
9898
});
9999
};

express-zod-api/tests/__snapshots__/env.spec.ts.snap

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -249,19 +249,23 @@ exports[`Environment checks > Zod imperfections > circular object schema has no
249249
}
250250
`;
251251

252-
exports[`Environment checks > Zod imperfections > meta overrides, does not merge 1`] = `
252+
exports[`Environment checks > Zod new features > input examples of transformations 1`] = `
253253
{
254-
"title": "last",
254+
"$schema": "https://json-schema.org/draft-2020-12/schema",
255+
"examples": [
256+
"test",
257+
],
258+
"type": "string",
255259
}
256260
`;
257261

258-
exports[`Environment checks > Zod new features > input examples of transformations 1`] = `
262+
exports[`Environment checks > Zod new features > meta() merge, not just overrides 1`] = `
259263
{
260-
"$schema": "https://json-schema.org/draft-2020-12/schema",
264+
"description": "some",
261265
"examples": [
262266
"test",
263267
],
264-
"type": "string",
268+
"title": "last",
265269
}
266270
`;
267271

express-zod-api/tests/__snapshots__/zts.spec.ts.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ exports[`zod-to-ts > z.optional() > Zod 4: should add question mark only to opti
274274
"{
275275
optional?: string | undefined;
276276
required: string;
277-
transform: number;
277+
transform?: number | undefined;
278278
or: number | string;
279279
tuple?: [
280280
string,

express-zod-api/tests/documentation-helpers.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ describe("Documentation helpers", () => {
177177

178178
describe("depictUnion()", () => {
179179
test("should set discriminator prop for such union", () => {
180-
const zodSchema = z.discriminatedUnion([
180+
const zodSchema = z.discriminatedUnion("status", [
181181
z.object({ status: z.literal("success"), data: z.any() }),
182182
z.object({
183183
status: z.literal("error"),

express-zod-api/tests/env.spec.ts

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ describe("Environment checks", () => {
3434
test("discriminated unions are not depicted well", () => {
3535
expect(
3636
z.toJSONSchema(
37-
z.discriminatedUnion([
37+
z.discriminatedUnion("status", [
3838
z.object({ status: z.literal("success"), data: z.any() }),
3939
z.object({
4040
status: z.literal("error"),
@@ -50,15 +50,6 @@ describe("Environment checks", () => {
5050
expect(R.omit(["$schema"], json)).toEqual({});
5151
});
5252

53-
test("meta overrides, does not merge", () => {
54-
const schema = z
55-
.string()
56-
.meta({ examples: ["test"] })
57-
.meta({ description: "some" })
58-
.meta({ title: "last" });
59-
expect(schema.meta()).toMatchSnapshot();
60-
});
61-
6253
test("circular object schema has no sign of getter in its shape", () => {
6354
const schema = z.object({
6455
name: z.string(),
@@ -88,6 +79,15 @@ describe("Environment checks", () => {
8879
});
8980

9081
describe("Zod new features", () => {
82+
test("meta() merge, not just overrides", () => {
83+
const schema = z
84+
.string()
85+
.meta({ examples: ["test"] })
86+
.meta({ description: "some" })
87+
.meta({ title: "last" });
88+
expect(schema.meta()).toMatchSnapshot();
89+
});
90+
9191
test("object shape conveys the keys optionality", () => {
9292
const schema = z.object({
9393
one: z.boolean(),
@@ -104,16 +104,19 @@ describe("Environment checks", () => {
104104
"three",
105105
"four",
106106
]);
107-
expect(schema._zod.def.shape.one._zod.optionality).toBeUndefined();
108-
expect(schema._zod.def.shape.two._zod.optionality).toBe("optional");
109-
expect(schema._zod.def.shape.three._zod.optionality).toBe("defaulted");
110-
/** @link https://github.com/colinhacks/zod/issues/4322 */
111-
expect(schema._zod.def.shape.four._zod.optionality).not.toBe("optional"); // <— undefined
107+
expect(schema._zod.def.shape.one._zod.optin).toBeUndefined();
108+
expect(schema._zod.def.shape.one._zod.optout).toBeUndefined();
109+
expect(schema._zod.def.shape.two._zod.optin).toBe("optional");
110+
expect(schema._zod.def.shape.two._zod.optout).toBe("optional");
111+
expect(schema._zod.def.shape.three._zod.optin).toBe("optional");
112+
expect(schema._zod.def.shape.three._zod.optout).toBe(undefined);
113+
expect(schema._zod.def.shape.four._zod.optin).toBe("optional");
114+
expect(schema._zod.def.shape.four._zod.optout).toBe(undefined);
112115
expectTypeOf<z.input<typeof schema>>().toEqualTypeOf<{
113116
one: boolean;
114117
two?: boolean | undefined;
115118
three?: boolean | undefined;
116-
four: boolean | undefined;
119+
four?: boolean | undefined;
117120
}>();
118121
expectTypeOf<z.output<typeof schema>>().toEqualTypeOf<{
119122
one: boolean;

express-zod-api/tests/zts.spec.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -207,10 +207,6 @@ describe("zod-to-ts", () => {
207207
expect(printNodeTest(node)).toMatchSnapshot();
208208
});
209209

210-
/**
211-
* @todo revisit when optional+transform fixed
212-
* @link https://github.com/colinhacks/zod/issues/4322
213-
* */
214210
test("Zod 4: should add question mark only to optional props", () => {
215211
const node = zodToTs(objectWithOptionals, { ctx });
216212
expect(printNodeTest(node)).toMatchSnapshot();

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
"typescript": "^5.8.3",
4949
"typescript-eslint": "^8.32.1",
5050
"vitest": "^3.1.3",
51-
"zod": "3.25.0-beta.20250518T002810"
51+
"zod": "3.25.0-beta.20250519T051133"
5252
},
5353
"resolutions": {
5454
"**/@scarf/scarf": "npm:[email protected]"

yarn.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3123,7 +3123,7 @@ yocto-queue@^0.1.0:
31233123
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
31243124
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
31253125

3126-
[email protected].20250518T002810:
3127-
version "3.25.0-beta.20250518T002810"
3128-
resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.0-beta.20250518T002810.tgz#9ac105aea4b6e0d072a382893c3e6556670048c8"
3129-
integrity sha512-3/aIqMbUXG9EjTelJkDcWd+izJP5MxFgQEMSYI8n41pwYhRDYYxy2dnbkgfNcnLbFZ9uByZn9XXqHTh05QHqSQ==
3126+
[email protected].20250519T051133:
3127+
version "3.25.0-beta.20250519T051133"
3128+
resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.0-beta.20250519T051133.tgz#e2bf36f81255419f761f591ca983b792beb5ac0a"
3129+
integrity sha512-kiLlHJfDDF2JJ2RcCvxhysRDnrSEHtOuHMLtG+PESvLEScEujB8ccvZ6KgvBxS1ifwpjlL/hkjqwvDzKKoryjQ==

0 commit comments

Comments
 (0)