logo
Basic Utils
Home
postiz
  • Zod v4: Faster, Smarter, and More TypeScript-Friendly

    Table of Contents

    1. Introduction To The Tutorial
    2. Performance Enhancements
    3. @zod/mini: A Lightweight Alternative
    4. Metadata in Zod v4
    5. The Global Registry in Zod v4
    6. JSON Schema Conversion in Zod v4
    7. z.interface()in Zod v4
    8. File Schemas in Zod v4
    9. Internationalizationin Zod v4
    10. Error pretty-printingin Zod v4
    11. Custom Email Regex in Zod v4
    12. Template Literal Types in Zod v4
    13. Number Formats in Zod v4
    14. Stringbool in Zod v4
    15. Simplified Error Customization in Zod v4
    16. Upgraded z.discriminatedUnion() in Zod v4
    17. Multiple Values in z.literal() in Zod v4
    18. Refinements Inside Schemas in Zod v4
    19. .overwrite() in Zod v4
    20. @zod/core
    21. Conclusion
    zod-logo
    zod-logo
    basicutils.com

    Introduction To The Tutorial

    In the ever-evolving world of TypeScript validation, Zod has emerged as the strongest and favorite among most TS developers. Its simplicity and power have captivated many. Zod has announced that they are releasing Zod v4! This is a significant leap, given the great changes they have added to the validation framework. It’s faster, smarter, and enhanced.

    In this article, we will consider the new features of Zod v4. We will explore how Zod has become more TypeScript-friendly, its performance benchmarks, and break down the breaking changes. You can take it to be a tutorial on Zod v4.

    If you have been using Zod—or even considering using it—this is worth a closer look. Let's go!

    Performance Enhancements

    One of the largest improvements in Zod v4 is efficiency and speed. The engine can now work with deeply nested structures significantly faster. Several benchmarks have been used to test its speed. These include:

    • String parsing benchmark
    • Array parsing benchmark
    • Object parsing benchmark

    To test them yourself, follow this procedure:

    $ git clone git@github.com:colinhacks/zod.git
    $ cd zod
    $ git switch v4
    $ pnpm install
    $ pnpm bench <name>
    

    Replace <name> with the appropriate benchmark name. It can be any of the following: string, array, object-moltar.

    Sample Result from Array Parsing Benchmark

    $ pnpm bench array
    runtime: node v22.13.0 (arm64-darwin)
     
    benchmark      time (avg)             (min … max)       p75       p99      p999
    ------------------------------------------------- -----------------------------
    • z.array() parsing
    ------------------------------------------------- -----------------------------
    zod3          162 µs/iter       (141 µs … 753 µs)    152 µs    291 µs    513 µs
    zod4       54'282 ns/iter    (47'084 ns … 669 µs) 50'833 ns    185 µs    233 µs
     
    summary for z.array() parsing
      zod4
       2.98x faster than zod3
    

    Findings from the Benchmarks

    The following observations were made during testing:

    • Reduced validation overhead – Faster schema parsing and execution
    • Smarter type inference – More efficient TypeScript type handling
    • Improved memory usage – Optimized internal operations for better performance

    Zod v4 brings major speed improvements, ensuring developers can validate data faster and more efficiently. In the following sections, we will cover the major additions in Zod v4.

    @zod/mini: A Lightweight Alternative

    @zod/mini is a lightweight Zod version designed for projects that require minimal bundle size while maintaining core validation functionalities.

    Why @zod/mini?

    • Optimized for smaller apps
    • Reduced bundle size
    • Maintains essential validation features

    How to Use @zod/mini

    Here is how to install @zod/mini:

    npm install @zod/mini@next
    

    And this is how you would use it:

    import * as z from "@zod/mini";
     
    z.optional(z.string());
     
    z.union([z.string(), z.number()]);
     
    z.extend(z.object({ /* ... */ }), { age: z.number() });
    

    Metadata in Zod v4

    Zod v4 introduces a new way for handling metadata using a schema registry. Rather than storing the metadata inside the schema itself, Zod stores it in a separate registry, making metadata management easier.

    How It Works

    import * as z from "zod";
     
    const myRegistry = z.registry<{ title: string; description: string }>();
    

    Adding Schemas to Your Registry

    const emailSchema = z.string().email();
     
    myRegistry.add(emailSchema, { title: "Email address", description: "..." });
    myRegistry.get(emailSchema);
    // => { title: "Email address", ... }
    

    Using .register() for Convenience

    Alternatively, you can also use the .register() method for convenience:

    emailSchema.register(myRegistry, { title: "Email address", description: "..." })
    // => returns emailSchema
    

    The Global Registry in Zod v4

    Zod v4 introduces z.globalRegistry, a centralized registry for storing common JSON Schema-compatible metadata. This feature helps developers keep track of reusable schema descriptions across applications.

    Adding Metadata to the Global Registry

    z.globalRegistry.add(z.string(), { 
      id: "email_address",
      title: "Email address",
      description: "Provide your email",
      examples: ["naomie@example.com"],
      extraKey: "Additional properties are also allowed"
    });
    

    Using .meta() for Convenience

    Instead of manually adding schemas to the global registry, you can attach metadata directly using .meta():

    z.string().meta({ 
      id: "email_address",
      title: "Email address",
      description: "Provide your email",
      examples: ["naomie@example.com"],
    });
    

    JSON Schema Conversion in Zod v4

    Zod v4 brings improved JSON Schema conversion, making it easier to integrate with JSON-based validation tools and APIs.

    Why JSON Schema Conversion Matters

    Helps interop with other validation libraries

    Allows schemas to be shared across backend and frontend

    Supports auto-generating API documentation

    Converting a Zod Schema to JSON Schema

    Zod v4 provides a .toJSONSchema() method to convert a schema:

    const userSchema = z.object({
      name: z.string(),
      age: z.number().optional(),
    });
    const jsonSchema = userSchema.toJSONSchema();
    console.log(jsonSchema);
    

    Example output:

    {
      "type": "object",
      "properties": {
        "name": { "type": "string" },
        "age": { "type": "number" }
      },
      "required": ["name"]
    }
    

    z.interface()in Zod v4

    Zod v4 introduces z.interface(), a new way to define object schemas with better optional property handling and true recursive types.

    Exact(er) Optional Properties

    In TypeScript, a property can be optional in two ways:

    • Key optional: The key may be omitted.

    Value optional: The key is required, but the value can be undefined.

    Zod 3 couldn’t fully differentiate between these, so Zod 4 adds z.interface(), allowing precise control:

    const ValueOptional = z.interface({ name: z.string().optional()}); 
    // { name: string | undefined }
    const KeyOptional = z.interface({ "name?": z.string() }); 
    // { name?: string }
    

    Here, the ? suffix defines key optionality, making schema behavior more predictable.

    True Recursive Types

    Zod v4 also simplifies defining recursive structures. Previously, developers had to use z.lazy() and extra TypeScript casting. Now, z.interface() allows recursive properties naturally using getters:

    const Category = z.interface({
      name: z.string(),
      get subcategories() {
        return z.array(Category);
      }
    });
    

    This eliminates the need for workarounds, making recursive schemas cleaner.

    File Schemas in Zod v4

    Zod v4 introduces file validation, allowing developers to define schemas for File instances with constraints on size and type.

    Validating File Instances

    You can create a file schema using z.file():

    const fileSchema = z.file();
     
    fileSchema.min(10_000); // minimum .size (bytes)
    fileSchema.max(1_000_000); // maximum .size (bytes)
    

    fileSchema.type("image/png"); // MIME type

    International - ization in Zod v4

    Zod v4 introduces a new way of working with multiple languages:

    import * as z from "zod";
     
    // configure English locale (default)
    z.config(z.locales.en());
    

    Error pretty-printingin Zod v4

    Zod now internally provides an API similar to zod-validation-error. The z.prettifyError function converts ZodError into a friendly, formatted string. Simply pass your error to the function, and Zod handles the rest.

    z.prettifyError(myError);
    

    Custom Email Regex in Zod v4

    Zod v4 enhances email validation by allowing developers to define custom regular expressions for z.email(). Since there is no universally perfect email regex, different applications may require varying levels of strictness.

    Built-in Email Regex Options

    Zod provides several predefined regex patterns for common use cases:

    // Zod's default email validation (Gmail-style rules)
    z.email(); // Uses z.regexes.email
    // Browser-standard email validation (HTML5)
    z.email({ pattern: z.regexes.html5Email });
    // Classic email regex following RFC 5322
    z.email({ pattern: z.regexes.rfc5322Email });
    // Loose validation supporting Unicode characters
    z.email({ pattern: z.regexes.unicodeEmail });
    

    Template Literal Types in Zod v4

    This allows developers to define complex string formats in a structured way.

    Defining Template Literal Types

    You can create dynamic string patterns using z.templateLiteral():

    const hello = z.templateLiteral(["hello, ", z.string()]);
    // `hello, ${string}`
     
    const cssUnits = z.enum(["px", "em", "rem", "%"]);
    const css = z.templateLiteral([z.number(), cssUnits]);
    // `${number}px` | `${number}em` | `${number}rem` | `${number}%`
     
    const email = z.templateLiteral([
      z.string().min(1),
      "@",
      z.string().max(64),
    ]);
    // `${string}@${string}` (the min/max refinements are enforced!)
    

    Number Formats in Zod v4

    Zod v4 introduces new numeric formats for handling fixed-width integer and float types with built-in constraints.

    .Standard Number Formats

    • These formats ensure proper min/max constraints for standard numbers:
    z.int();      // [Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]
    z.float32();  // [-3.4028234663852886e38, 3.4028234663852886e38]
    z.float64();  // [-1.7976931348623157e308, 1.7976931348623157e308]
    z.int32();    // [-2147483648, 2147483647]
    z.uint32();   // [0, 4294967295]
    

    Stringbool in Zod v4

    Zod v4 introduces z.stringbool(), which maps common truthy and falsy strings to actual true or false values.

    How to Use It

    const strbool = z.stringbool();
    strbool.parse("true");      // => true
    strbool.parse("1");         // => true
    strbool.parse("yes");       // => true
    strbool.parse("on");        // => true
    strbool.parse("enable");    // => true
    strbool.parse("false");     // => false
    strbool.parse("0");         // => false
    strbool.parse("no");        // => false
    strbool.parse("off");       // => false
    strbool.parse("disabled");  // => false
    

    Customizing Truthy and Falsy Values

    You can also customize the truthy and falsey values like below:

    z.stringbool({
      truthy: ["yes", "true"],
      falsy: ["no", "false"]
    });
    

    Simplified Error Customization in Zod v4

    Zod v4 introduces a major change in error customization, replacing multiple error parameters with a single error function.

    Comparing Zod 3 and Zod 4 Error Handling

    // Zod 3
    - z.string({ 
    -   required_error: "This field is required" 
    -   invalid_type_error: "Not a string", 
    - });
     
    // Zod 4 
    + z.string({ error: (issue) => issue.input === undefined ? 
    +  "This field is required" :
    +  "Not a string" 
    + });
    

    And here is how we would handle error maps in In zod 3 and 4 for comparison:

    // Zod 3 
    - z.string({
    -   errorMap: (issue, ctx) => {
    -     if (issue.code === "too_small") {
    -       return { message: `Value must be >${issue.minimum}` };
    -     }
    -     return { message: ctx.defaultError };
    -   },
    - });
     
    // Zod 4
    + z.string({
    +   error: (issue) => {
    +     if (issue.code === "too_small") {
    +       return `Value must be >${issue.minimum}`
    +     }
    +   },
    + });
    

    Upgraded z. discriminated - Union() in Zod v4

    Zod v4 makes discriminated unions smarter and easier to use.

    Automatic Discriminator Key Detection

    Previously, you had to explicitly specify the discriminator key. Now, Zod automatically identifies it. If no shared key is found, an error is thrown at schema initialization.

    // In Zod 4 (automatic key detection)
    const myUnion = z.discriminatedUnion([
      z.object({ type: z.literal("a"), a: z.string() }),
      z.object({ type: z.literal("b"), b: z.number() }),
    ]);
    // In Zod 3 (manual key specification)
    const myUnion = z.discriminatedUnion("type", [
      z.object({ type: z.literal("a"), a: z.string() }),
      z.object({ type: z.literal("b"), b: z.number() }),
    ]);
    

    Composing Discriminated Unions

    Zod now allows nested discriminated unions, meaning one can be used inside another. Zod chooses the best strategy automatically.

    const BaseError = z.object({ status: z.literal("failed"), message: z.string() });
    const MyErrors = z.discriminatedUnion([
      BaseError.extend({ code: z.literal(400) }),
      BaseError.extend({ code: z.literal(401) }),
      BaseError.extend({ code: z.literal(500) }),
    ]);
    const MyResult = z.discriminatedUnion([
      z.interface({ status: z.literal("success"), data: z.string() }),
      MyErrors
    ]);
    

    Multiple Values in z.literal() in Zod v4

    Zod v4's z.literal() now supports multiple values, making it more flexible compared to previous versions.

    The code that follows demonstrates how to deal with multiple values in z.literal in both versions 3 and 4.

    const httpCodes = z.literal([ 200, 201, 202, 204, 206, 207, 208, 226 ]);
     
    // previously in Zod 3:
    const httpCodes = z.union([
      z.literal(200),
      z.literal(201),
      z.literal(202),
      z.literal(204),
      z.literal(206),
      z.literal(207),
      z.literal(208),
      z.literal(226)
    ]);
    

    Refinements Inside Schemas in Zod v4

    Previously in Zod 3, refinements were stored in a separate ZodEffects class, which made it impossible to mix .refine() with other schema methods like .min(). This led to frustrating limitations when chaining validations.

    Old Behavior in Zod 3 (Issue with Refinements)

    z.string()
      .refine(val => val.includes("@"))
      .min(5); 
    // ❌ Property 'min' does not exist on type ZodEffects<ZodString, string, string>
    

    New Behavior in Zod 4 (Refinements Inside Schemas)

    Now, refinements are directly part of the schema, allowing seamless chaining with other methods:

    z.string()
      .refine(val => val.includes("@"))
      .min(5); 
    // ✅ Works as expected!
    

    .overwrite() in Zod v4

    Zod v4 introduces .overwrite(), a new method for defining transformations without altering the inferred type.

    Why .overwrite()?

    Previously, .transform() allowed arbitrary changes to a value, but it made type introspection impossible at runtime. This meant schemas couldn’t be cleanly converted to JSON Schema.

    Example of .transform() losing type introspection:

    const Squared = z.number().transform(val => val ** 2);
    // => ZodPipe<ZodNumber, ZodTransform> (type introspection lost)
    

    How .overwrite() Works

    Unlike .transform(), .overwrite() keeps the original type intact:

    z.number().overwrite(val => val ** 2).max(100);
    // => ZodNumber (type remains intact)
    

    @zod/core

    Zod v4 introduces @zod/core, a package that contains the core validation functionality shared between Zod and @zod/mini. While not directly relevant for most Zod users, this addition significantly expands Zod’s capabilities.

    Why @zod/core Exists

    • The creation of @zod/mini (a lightweight version of Zod) required a shared foundational package.
    • @zod/core makes Zod more modular, allowing developers to build custom schema libraries on top of it.
    • Zod evolves from being just a validation library to a flexible validation engine that can power other frameworks.

    Who Should Use @zod/core?

    If you’re building a schema validation library, you can refer to the implementations of Zod and @zod/mini to see how they use @zod/core as a foundation. Developers are encouraged to discuss ideas and seek guidance on GitHub or platforms like X/Bluesky.

    Conclusion

    Zod v4 is a massive upgrade that brings better performance, smarter validation, and stronger TypeScript integration. From precise optional properties and recursive types to enhanced JSON Schema conversion and file validation, this version makes schema definition more powerful and flexible than ever.

    With new features like z.interface(), @zod/mini, and an extensible core, Zod isn’t just a validation library—it’s evolving into a foundational tool for TypeScript projects. Whether you're working on API validation, form handling, or complex data structures, Zod v4 empowers developers to write safer, more predictable code.

    If you're already using Zod—or thinking about it—this update is worth exploring. The improvements make validation faster, easier, and more intuitive, solidifying Zod as the go-to choice for TypeScript validation.

    Frequently Asked Questions

    Zod v4 introduces significant enhancements, including better TypeScript integration, improved error handling, automatic discriminator detection in unions, and new features like z.interface() for precise optional property control.

    z.stringbool() converts common truthy and falsy strings into actual boolean values. It maps 'true', 'yes', 'on', 'enable' to true, while 'false', 'no', 'off', 'disabled' convert to false.

    In Zod v4, z.literal() supports multiple values in a single schema, making it easier to define constant sets and simplifying union definitions.

    Zod v4 consolidates error customization into a single error function, allowing dynamic error messages based on validation issues, replacing multiple error parameters.

    z.prettifyError converts ZodError into a human-friendly formatted string, making debugging easier by providing clearer validation feedback.

    Zod v4 introduces locale-based error customization through z.config(), allowing developers to configure validation messages in different languages.

    The schema registry enables storing metadata separately from schemas, making metadata management more efficient across large applications.

    z.globalRegistry provides a centralized location for storing common metadata, ensuring consistency across applications using JSON Schema-compatible descriptions.

    Zod v4 enhances JSON Schema conversion by ensuring better compatibility with validation tools, supporting frontend-backend schema sharing, and enabling automatic API documentation generation.

    Zod v4 analyzes schema definitions and identifies the discriminator key automatically. If no shared discriminator is found, an error is thrown at schema initialization.

    Developers can define custom mappings for truthy and falsy values using an options object, specifying which strings should be treated as true or false.

    The error function replaces traditional error mapping, allowing direct customization of error messages based on validation issues.

    Zod v3 used separate error parameters like required_error and invalid_type_error, while Zod v4 unifies them under a single error function.

    The ability to define multiple literals in a single schema reduces complexity in defining constant unions and makes schemas more concise.

    Zod v4's improvements make it ideal for API validation, frontend-backend schema synchronization, error customization, and building modular validation frameworks.

    References

    Background References

    1. (April 15, 2025). Introducing Zod 4 beta. *Zod*. Retrieved April 15, 2025 from https://v4.zod.dev/v4
    2. (April 14, 2025). Migration guide. *Zod*. Retrieved April 14, 2025 from https://v4.zod.dev/v4/changelog

    About the Author

    Joseph Horace's photo

    Joseph Horace

    Horace is a dedicated software developer with a deep passion for technology and problem-solving. With years of experience in developing robust and scalable applications, Horace specializes in building user-friendly solutions using cutting-edge technologies. His expertise spans across multiple areas of software development, with a focus on delivering high-quality code and seamless user experiences. Horace believes in continuous learning and enjoys sharing insights with the community through contributions and collaborations. When not coding, he enjoys exploring new technologies and staying updated on industry trends.

    logo
    Basic Utils

    simplify and inspire technology

    ©2024, basicutils.com