When using Zod v4's z.string().exactOptional() inside a z.object(), the generated OpenAPI schema incorrectly includes the property in the required array. This is because isOptionalSchema() in src/lib/zod-is-type.ts uses schema.safeParse(undefined).success to determine if a property is optional, but ZodExactOptional rejects undefined as a value while still allowing the key to be absent from the object.
Expected behavior
Properties defined with .exactOptional() should not be included in the required array of the OpenAPI schema, because exactOptional allows the property key to be absent from the object.
In JSON/OpenAPI terms, required means "this key must be present in the JSON object". Since exactOptional allows key absence, it should be treated as not required — the same as .optional().
Current behavior
Properties defined with .exactOptional() are included in the required array.
Reproduction
import { z } from "zod/v4";
import {
OpenAPIRegistry,
OpenApiGeneratorV31,
extendZodWithOpenApi,
} from "@asteasolutions/zod-to-openapi";
extendZodWithOpenApi(z);
const registry = new OpenAPIRegistry();
const schema = z.object({
name: z.string(),
nickname: z.string().optional(),
bio: z.string().exactOptional(),
});
registry.register("User", schema);
const generator = new OpenApiGeneratorV31(registry.definitions);
const doc = generator.generateDocument({
openapi: "3.1.0",
info: { title: "Test", version: "1.0.0" },
});
const userSchema = doc.components?.schemas?.User as any;
console.log("required:", userSchema.required);
// Actual: required: ["name", "bio"]
// Expected: required: ["name"]
Root cause
In src/lib/zod-is-type.ts:
export function isOptionalSchema(schema: z.ZodType) {
return schema.safeParse(undefined).success;
}
This function is called from src/transformers/object.ts to determine which properties to include in the required array:
private requiredKeysOf(objectSchema: ZodObject) {
return Object.entries(objectSchema.def.shape)
.filter(([_key, type]) => !isOptionalSchema(type))
.map(([key, _type]) => key);
}
The issue:
z.string().optional().safeParse(undefined) → success: true → correctly excluded from required
z.string().exactOptional().safeParse(undefined) → success: false → incorrectly included in required
ZodExactOptional semantics in Zod v4:
- Key absence is allowed (the property is optional in the object)
- Value
undefined is not allowed (if the key is present, it must be the actual type)
Since JSON has no concept of undefined, both .optional() and .exactOptional() should map to "not required" in OpenAPI.
Suggested fix
Update isOptionalSchema to also check for ZodExactOptional:
export function isOptionalSchema(schema: z.ZodType) {
if (schema.safeParse(undefined).success) return true;
// ZodExactOptional allows key absence but rejects undefined as a value.
// In JSON/OpenAPI, this still means the property is not required.
if ((schema as any)?.def?.type === "exactOptional") return true;
return false;
}
Environment
zod: 4.3.6
@asteasolutions/zod-to-openapi: 8.4.1
@hono/zod-openapi: 1.2.2
exactOptionalPropertyTypes: true in tsconfig.json
When using Zod v4's
z.string().exactOptional()inside az.object(), the generated OpenAPI schema incorrectly includes the property in therequiredarray. This is becauseisOptionalSchema()insrc/lib/zod-is-type.tsusesschema.safeParse(undefined).successto determine if a property is optional, butZodExactOptionalrejectsundefinedas a value while still allowing the key to be absent from the object.Expected behavior
Properties defined with
.exactOptional()should not be included in therequiredarray of the OpenAPI schema, becauseexactOptionalallows the property key to be absent from the object.In JSON/OpenAPI terms,
requiredmeans "this key must be present in the JSON object". SinceexactOptionalallows key absence, it should be treated as not required — the same as.optional().Current behavior
Properties defined with
.exactOptional()are included in therequiredarray.Reproduction
Root cause
In
src/lib/zod-is-type.ts:This function is called from
src/transformers/object.tsto determine which properties to include in therequiredarray:The issue:
z.string().optional().safeParse(undefined)→success: true→ correctly excluded fromrequiredz.string().exactOptional().safeParse(undefined)→success: false→ incorrectly included inrequiredZodExactOptionalsemantics in Zod v4:undefinedis not allowed (if the key is present, it must be the actual type)Since JSON has no concept of
undefined, both.optional()and.exactOptional()should map to "not required" in OpenAPI.Suggested fix
Update
isOptionalSchemato also check forZodExactOptional:Environment
zod: 4.3.6@asteasolutions/zod-to-openapi: 8.4.1@hono/zod-openapi: 1.2.2exactOptionalPropertyTypes: trueintsconfig.json