---
title: "Zod 4 is stable: faster parsing and what changes in your schemas · vuejs.school"
description: "Zod 4.0.0 is on npm. Parsing is several times faster, the bundle is half the size, and error messages, objects and string formats change. Here is the upgrade."
canonical_url: "https://vuejs.school/community/blog/zod-4-is-stable-what-changes-in-your-schemas"
meta:
  "og:description": "Zod 4.0.0 is on npm. Parsing is several times faster, the bundle is half the size, and error messages, objects and string formats change. Here is the upgrade."
  "og:image": "https://vuejs.school/_og/d/a_~Wm9kIDQgaXMgc3RhYmxlOiBmYXN0ZXIgcGFyc2luZyBhbmQgd2hhdCBjaGFuZ2VzIGluIHlvdXIgc2NoZW1hcyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~Wm9kIDQgaXMgc3RhYmxlOiBmYXN0ZXIgcGFyc2luZyBhbmQgd2hhdCBjaGFuZ2VzIGluIHlvdXIgc2NoZW1hcw%2Cdescription_~Wm9kIDQuMC4wIGlzIG9uIG5wbS4gUGFyc2luZyBpcyBzZXZlcmFsIHRpbWVzIGZhc3RlciwgdGhlIGJ1bmRsZSBpcyBoYWxmIHRoZSBzaXplLCBhbmQgZXJyb3IgbWVzc2FnZXMsIG9iamVjdHMgYW5kIHN0cmluZyBmb3JtYXRzIGNoYW5nZS4gSGVyZSBpcyB0aGUgdXBncmFkZS4%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA3IG1pbiByZWFk%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy96b2QtNC1pcy1zdGFibGUtd2hhdC1jaGFuZ2VzLWluLXlvdXItc2NoZW1hcyI%2Cs_8V9O_8_iV7gxYZ-z.png"
  "og:image:alt": "Zod 4 is stable: faster parsing and what changes in your schemas · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "Zod 4 is stable: faster parsing and what changes in your schemas · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~Wm9kIDQgaXMgc3RhYmxlOiBmYXN0ZXIgcGFyc2luZyBhbmQgd2hhdCBjaGFuZ2VzIGluIHlvdXIgc2NoZW1hcyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~Wm9kIDQgaXMgc3RhYmxlOiBmYXN0ZXIgcGFyc2luZyBhbmQgd2hhdCBjaGFuZ2VzIGluIHlvdXIgc2NoZW1hcw%2Cdescription_~Wm9kIDQuMC4wIGlzIG9uIG5wbS4gUGFyc2luZyBpcyBzZXZlcmFsIHRpbWVzIGZhc3RlciwgdGhlIGJ1bmRsZSBpcyBoYWxmIHRoZSBzaXplLCBhbmQgZXJyb3IgbWVzc2FnZXMsIG9iamVjdHMgYW5kIHN0cmluZyBmb3JtYXRzIGNoYW5nZS4gSGVyZSBpcyB0aGUgdXBncmFkZS4%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA3IG1pbiByZWFk%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy96b2QtNC1pcy1zdGFibGUtd2hhdC1jaGFuZ2VzLWluLXlvdXItc2NoZW1hcyI%2Cs_8V9O_8_iV7gxYZ-z.png"
  "twitter:image:alt": "Zod 4 is stable: faster parsing and what changes in your schemas · vuejs.school"
---

> Index of every page on this site: [llms.txt](https://vuejs.school/llms.txt)

## What shipped

The release notes lead with numbers from the project's own benchmarks. Parsing a string is 14.71 times faster than Zod 3, an array 7.43 times, an object 6.5 times. TypeScript work drops too: a simple `.extend()` call that cost the compiler more than 25,000 type instantiations now costs about 175, and a chain of `.extend()` and `.omit()` calls that took 4,000 ms to check takes 400 ms. The core bundle shrinks from 12.47 kB to 5.36 kB gzipped for a schema with one boolean.

The feature list is long. The ones a Vue app will meet first:

- One `error` parameter replaces `message`, `invalid_type_error`, `required_error` and `errorMap`.
- String formats become top-level functions: `z.email()`, `z.uuid()`, `z.url()`, `z.iso.datetime()`. The method forms such as `z.string().email()` still work but are deprecated.
- `z.strictObject()` and `z.looseObject()` replace `.strict()` and `.passthrough()`.
- Recursive schemas use getters, with no type casting.
- `z.toJSONSchema()` converts a schema to JSON Schema, and `.meta()` attaches metadata.
- `z.file()` for file inputs, `z.int()` and the sized number formats, `z.stringbool()` for `"true"` and `"off"` style strings, template literal schemas, and `z.prettifyError()` for readable output.
- Zod Mini, at `zod/mini`, is a tree-shakable variant with functions instead of methods and weighs 1.88 kB for the same test.

## Which import gives you Zod 4

Zod 3.25.0, published on 19 May, already contained Zod 4 at the `zod/v4` subpath, next to Zod 3 at the root. The [versioning page](https://zod.dev/v4/versioning) explains why. Many libraries accept your schemas as input, and a Zod 3 type is not assignable to a Zod 4 type, so releasing a major straight away would have forced a chain of major releases on every one of them. With the subpath, libraries could support both with one peer range, and you could import whichever you wanted.

With 4.0.0 the root export becomes Zod 4 and Zod 3 moves to `zod/v3`. The subpaths stay, so three imports are now valid and all of them are stable:

```
import { z } from 'zod'      // Zod 4 since 4.0.0
import { z as z4 } from 'zod/v4' // also Zod 4, keeps working
import { z as z3 } from 'zod/v3' // Zod 3, for code you have not migrated
```

## Move custom messages to the `error` key

The release notes say most of Zod 4's breaking changes are in error customisation. Zod 3 had four ways to attach a message, and the two that distinguished "missing" from "wrong type" are gone. A typical sign-up schema from a Zod 3 project:

*src/schemas/signup.ts**Breaks***

```
import { z } from 'zod'

export const signupSchema = z.object({
  email: z
    .string({ required_error: 'Email is required' })
    .email({ message: 'Enter a valid email address' }),
  password: z.string().min(8, { message: 'Use at least 8 characters' }),
  newsletter: z.boolean().default(false),
})
```

On Zod 4 the `message` keys still compile (they are deprecated but accepted), and `required_error` fails to typecheck because it no longer exists in the parameter type. In a plain JavaScript file nothing fails: Zod ignores the key, and a missing email gets the default message, "Invalid input: expected string, received undefined".

The replacement is a single `error` key that takes a string or a function. The function receives the issue, and `issue.input === undefined` is how you tell a missing field from a wrong one:

*src/schemas/signup.ts**Works***

```
import { z } from 'zod'

export const signupSchema = z.object({
  email: z.email({
    error: (issue) => (issue.input === undefined ? 'Email is required' : 'Enter a valid email address'),
  }),
  password: z.string().min(8, { error: 'Use at least 8 characters' }),
  newsletter: z.boolean().default(false),
})

export type Signup = z.infer<typeof signupSchema>
```

Two other things moved here. `z.email()` is now a schema in its own right rather than a refinement on `z.string()`, and it accepts the same `error` option, so the message covers both "not a string" and "not an email". And `.refine()` now returns the schema's own type, so you can write `z.string().refine(noSpaces).min(3)` in that order; Zod 3 wrapped refined schemas in `ZodEffects`, which hid `.min()`.

One precedence rule changed with it: an `error` set on the schema now wins over an error map passed to `.parse()`, which is the opposite of Zod 3.

## Reading errors into a template

`ZodError.format()` and `.flatten()` are deprecated. Their replacements are standalone functions, `z.treeifyError()` for nested schemas and `z.flattenError()` for the flat, one-level object a form usually is. `flattenError` returns `formErrors` for issues at the top and `fieldErrors`, a map from field name to an array of messages, which is exactly the shape a template wants:

*src/components/SignupForm.vue*

```
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { z } from 'zod'
import { signupSchema, type Signup } from '@/schemas/signup'

const form = reactive({ email: '', password: '', newsletter: false })
const errors = ref<Partial<Record<keyof Signup, string[]>>>({})
const emit = defineEmits<{ submit: [data: Signup] }>()

function submit() {
  const result = signupSchema.safeParse(form)
  if (!result.success) {
    errors.value = z.flattenError(result.error).fieldErrors
    return
  }
  errors.value = {}
  emit('submit', result.data)
}
</script>

<template>
  <form novalidate @submit.prevent="submit">
    <label for="email">Email</label>
    <input id="email" v-model="form.email" type="email" :aria-invalid="Boolean(errors.email)">
    <p v-if="errors.email" role="alert">{{ errors.email[0] }}</p>

    <label for="password">Password</label>
    <input id="password" v-model="form.password" type="password">
    <p v-if="errors.password" role="alert">{{ errors.password[0] }}</p>

    <label><input v-model="form.newsletter" type="checkbox"> Send me the newsletter</label>
    <button type="submit">Create account</button>
  </form>
</template>
```

Each field maps to an array because one value can fail more than one check; the template shows the first message. `novalidate` turns off the browser's own email check, so the message people see is Zod's.

`safeParse` is unchanged: it returns `{ success, data }` or `{ success, error }` and does not throw on invalid input. `result.data` is typed as `Signup`, with `newsletter` a `boolean` rather than `boolean | undefined`, because `.default()` applies to the output type. If you were reading `error.errors`, switch to `error.issues`; the alias was removed.

## Objects, defaults and coercion

The object changes are small but they can alter parsed output, so read them before upgrading a schema that guards an API.

`.strict()` and `.passthrough()` are deprecated in favour of `z.strictObject()` and `z.looseObject()`, which say what they do at the point you read the schema. `.merge()` is deprecated in favour of `.extend()`. `.deepPartial()` and `.nonstrict()` are gone.

Defaults inside optional fields now apply: `z.object({ a: z.string().default('x').optional() })` parses `{}` to `{ a: 'x' }`, where Zod 3 returned `{}`.

`.default()` also short-circuits: when the input is `undefined` it returns the default without running the rest of the schema, so the default must match the output type. If you relied on the default being parsed, for example a date string that a transform turns into a `Date`, use `.prefault()`, which keeps the old behaviour:

```
const settings = z.object({
  theme: z.enum(['light', 'dark']).default('light'),
  fontSize: z.coerce.number().default(16),
  updatedAt: z.string().transform((s) => new Date(s)).prefault('1970-01-01'),
})
```

`settings.parse({})` gives `theme: 'light'`, `fontSize: 16` and `updatedAt` as a `Date` for 1 January 1970: the prefault string went through the transform, while the two defaults were returned as they are.

The input type of every `z.coerce` schema is now `unknown`, where Zod 3 used the target type (`number` for `z.coerce.number()`). The key is still required: a missing `fontSize` would become `NaN` and fail, so the `.default(16)` is what makes it optional.

## Strings, numbers, enums and arrays

Beyond the move to top-level formats:

- `z.uuid()` is stricter and checks the version and variant bits. Use `z.guid()` for any 8-4-4-4-12 hex string.
- `.ip()` and `.cidr()` are gone. Use `z.ipv4()`, `z.ipv6()`, `z.cidrv4()` or `z.cidrv6()`.
- `z.number()` no longer accepts `Infinity`.
- `.int()` only accepts safe integers, and `z.int()` is the recommended spelling.
- `z.nativeEnum(Role)` is deprecated; `z.enum(Role)` now accepts a TypeScript enum directly.
- `.nonempty()` on an array now behaves like `.min(1)`, so `z.array(z.string()).nonempty()` infers `string[]` instead of `[string, ...string[]]`. Code typed against the tuple needs `z.tuple([z.string()], z.string())`.

## What stays the same

`z.object`, `z.string`, `z.number`, `z.array`, `z.union`, `.optional()`, `.nullable()`, `.min()`, `.max()`, `.transform()`, `.refine()`, `parse`, `safeParse`, `parseAsync` and `z.infer` all read and behave as before for everyday use. A schema file that never customised messages will likely compile unchanged, and deprecated methods such as `.strict()` and `.merge()` keep working. Zod 3 itself stays available at `zod/v3`, and the versioning page says it will keep getting bug fixes.

## Upgrade, or hold at 3.25

The upgrade itself is one command:

```
npm install zod@^4.0.0
```

Before you run it, check what else in your project depends on Zod. In a Vue form that is often VeeValidate's adapter. Its current release, `@vee-validate/zod` 4.15.1, declares a peer dependency of `zod@^3.24.0`. Installing `zod@4` next to it makes npm stop with a peer dependency conflict, and forcing past that hands the adapter schemas of a type it was not written for. The same applies to any other package that accepts your schemas as input.

If one of them does not accept 4.0.0, do not upgrade the root yet. Install `zod@^3.25.76`, keep the schemas you pass to the adapter importing from `zod`, and write new or standalone schemas against `zod/v4`. The versioning page says that import keeps working forever, so those files need no change when you upgrade later; only the schemas you pass to the adapter will need the edits above.

If only your own code uses your schemas, upgrade now. TypeScript flags the `required_error`, `invalid_type_error` and `errorMap` keys left to rewrite; `message` keys still work and can move to `error` when convenient. Then run your tests: the default, UUID and `Infinity` changes above can alter a result without any compile error.

**Check it stuck**3 questions from the Schema validation with Zod quiz.

[**Practice now**](https://vuejs.school/learn/forms/schema-validation-with-zod?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Tutorial · 6 min read**Accessible form errors: aria-describedby, live regions and focus**](https://vuejs.school/community/blog/accessible-form-errors) [Tutorial · 6 min read**Multi-step forms with a Pinia store**](https://vuejs.school/community/blog/multi-step-forms-with-a-pinia-store)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://vuejs.school/#website","@type":"WebSite","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school/","publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/zod-4-is-stable-what-changes-in-your-schemas#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-07-15","description":"Zod 4.0.0 is on npm. Parsing is several times faster, the bundle is half the size, and error messages, objects and string formats change. Here is the upgrade.","name":"Zod 4 is stable: faster parsing and what changes in your schemas","url":"https://vuejs.school/community/blog/zod-4-is-stable-what-changes-in-your-schemas","breadcrumb":{"@id":"https://vuejs.school/community/blog/zod-4-is-stable-what-changes-in-your-schemas#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/zod-4-is-stable-what-changes-in-your-schemas"]}],"primaryImageOfPage":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/#identity","@type":"Organization","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school","logo":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/community/blog/zod-4-is-stable-what-changes-in-your-schemas#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-07-15","description":"Zod 4.0.0 is on npm. Parsing is several times faster, the bundle is half the size, and error messages, objects and string formats change. Here is the upgrade.","headline":"Zod 4 is stable: faster parsing and what changes in your schemas","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~Wm9kIDQgaXMgc3RhYmxlOiBmYXN0ZXIgcGFyc2luZyBhbmQgd2hhdCBjaGFuZ2VzIGluIHlvdXIgc2NoZW1hcyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~Wm9kIDQgaXMgc3RhYmxlOiBmYXN0ZXIgcGFyc2luZyBhbmQgd2hhdCBjaGFuZ2VzIGluIHlvdXIgc2NoZW1hcw%2Cdescription_~Wm9kIDQuMC4wIGlzIG9uIG5wbS4gUGFyc2luZyBpcyBzZXZlcmFsIHRpbWVzIGZhc3RlciwgdGhlIGJ1bmRsZSBpcyBoYWxmIHRoZSBzaXplLCBhbmQgZXJyb3IgbWVzc2FnZXMsIG9iamVjdHMgYW5kIHN0cmluZyBmb3JtYXRzIGNoYW5nZS4gSGVyZSBpcyB0aGUgdXBncmFkZS4%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA3IG1pbiByZWFk%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy96b2QtNC1pcy1zdGFibGUtd2hhdC1jaGFuZ2VzLWluLXlvdXItc2NoZW1hcyI%2Cs_8V9O_8_iV7gxYZ-z.png","@type":["Article","BlogPosting"],"articleSection":["Release notes"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/zod-4-is-stable-what-changes-in-your-schemas#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/zod-4-is-stable-what-changes-in-your-schemas#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/zod-4-is-stable-what-changes-in-your-schemas#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"Zod 4 is stable: faster parsing and what changes in your schemas","position":2}]},{"@id":"https://vuejs.school/#logo","@type":"ImageObject","caption":"vuejs.school","contentUrl":"https://vuejs.school/apple-touch-icon.png","inLanguage":"en","url":"https://vuejs.school/apple-touch-icon.png"},{"@id":"https://vuejs.school/#/schema/image/1","@type":"ImageObject","contentUrl":"https://vuejs.school/_og/d/a_~Wm9kIDQgaXMgc3RhYmxlOiBmYXN0ZXIgcGFyc2luZyBhbmQgd2hhdCBjaGFuZ2VzIGluIHlvdXIgc2NoZW1hcyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~Wm9kIDQgaXMgc3RhYmxlOiBmYXN0ZXIgcGFyc2luZyBhbmQgd2hhdCBjaGFuZ2VzIGluIHlvdXIgc2NoZW1hcw%2Cdescription_~Wm9kIDQuMC4wIGlzIG9uIG5wbS4gUGFyc2luZyBpcyBzZXZlcmFsIHRpbWVzIGZhc3RlciwgdGhlIGJ1bmRsZSBpcyBoYWxmIHRoZSBzaXplLCBhbmQgZXJyb3IgbWVzc2FnZXMsIG9iamVjdHMgYW5kIHN0cmluZyBmb3JtYXRzIGNoYW5nZS4gSGVyZSBpcyB0aGUgdXBncmFkZS4%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA3IG1pbiByZWFk%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy96b2QtNC1pcy1zdGFibGUtd2hhdC1jaGFuZ2VzLWluLXlvdXItc2NoZW1hcyI%2Cs_8V9O_8_iV7gxYZ-z.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~Wm9kIDQgaXMgc3RhYmxlOiBmYXN0ZXIgcGFyc2luZyBhbmQgd2hhdCBjaGFuZ2VzIGluIHlvdXIgc2NoZW1hcyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~Wm9kIDQgaXMgc3RhYmxlOiBmYXN0ZXIgcGFyc2luZyBhbmQgd2hhdCBjaGFuZ2VzIGluIHlvdXIgc2NoZW1hcw%2Cdescription_~Wm9kIDQuMC4wIGlzIG9uIG5wbS4gUGFyc2luZyBpcyBzZXZlcmFsIHRpbWVzIGZhc3RlciwgdGhlIGJ1bmRsZSBpcyBoYWxmIHRoZSBzaXplLCBhbmQgZXJyb3IgbWVzc2FnZXMsIG9iamVjdHMgYW5kIHN0cmluZyBmb3JtYXRzIGNoYW5nZS4gSGVyZSBpcyB0aGUgdXBncmFkZS4%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA3IG1pbiByZWFk%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy96b2QtNC1pcy1zdGFibGUtd2hhdC1jaGFuZ2VzLWluLXlvdXItc2NoZW1hcyI%2Cs_8V9O_8_iV7gxYZ-z.png"}]}
```