Skip to content
advanced Phase 8 · Forms and Validation

Schema Validation with zod

Define typed schemas once and parse form data for both client and server.

45m
0 problems
Topic Progress 0%

Schema Validation with zod: Core Concepts

Describe the boundary with a schema

A Zod schema defines the shape and constraints of data at a boundary. A Vue form's reactive state is usually optimized for editing: a number field may be represented as text, optional controls may be blank, and values may be incomplete while the user types. A schema parses that raw object into a valid output or returns structured issues. This makes validation an explicit step between editing and using or sending the data.

import { z } from 'zod'

const workspaceSchema = z.object({
  name: z.string().trim().min(1, 'Enter a workspace name.'),
  seats: z.coerce.number().int().min(1).max(500),
  region: z.enum(['us-east', 'eu-west']),
})

type WorkspaceInput = z.input<typeof workspaceSchema>
type Workspace = z.output<typeof workspaceSchema>

The schema can normalize as well as validate. Trimming a name or coercing a numeric string means successful parsing returns a value that may differ from the form's raw input. That distinction matters in TypeScript: input and output types can be different, particularly with coercion and transforms. Use the inferred output type for code that consumes parsed data, and use the input type when describing the unparsed editing representation. A single inferred type does not always describe both sides accurately.

safeParse is useful in an interactive form because invalid user input is an expected result, not an exceptional control-flow event. It returns a success branch with typed data or a failure branch with issues. Each issue includes a path that can be mapped to a field. Keep the first useful message per field for a simple form, or retain multiple issues when the interface can present them clearly. Issues with an empty path can be shown as form-level errors.

Reuse rules without confusing trust boundaries

Sharing a schema between a client and server can reduce rule drift, especially in a TypeScript codebase with a shared package. It does not remove server-side validation: the server must parse the actual request because a browser is not trusted. Shared code can also require care when rules depend on environment-specific services, localization, or secrets. Keep the pure shape and deterministic constraints reusable, and layer authorization or database checks at the appropriate server boundary.

Schemas complement rather than replace reactive form state. v-model still updates values, and the component still decides when to run parsing, which issues to reveal, how to focus errors, and what to preserve after a failed save. A schema library knows data constraints; it does not automatically provide touched state, dirty tracking, accessible markup, or async request lifecycle. Decide whether to parse on blur, on submit, or after a previous error, and avoid surprising users by displaying every issue on every keystroke.

A schema is valuable when fields have shared rules, the form has nested data, or the parsed type should be enforced across multiple call sites. For a tiny form with one required string, a schema may add indirection without reducing duplication. Choose it when the structure and constraints have enough complexity to benefit from one declared contract.

Review schema changes like API changes: update boundary tests and callers when the accepted input or normalized output changes.

Schema Validation with zod: Practice

Parse and map issues in Vue

The following Vue example keeps editing values separate from parsed request data. It uses a number-like string while the person edits, lets the schema coerce it, and maps each issue path to a message. The result's data is used only after parsing succeeds.

<script setup lang="ts">
import { reactive, ref } from 'vue'
import { z } from 'zod'

const requestSchema = z.object({
  email: z.string().trim().email('Enter a valid email address.'),
  seats: z.coerce.number().int().min(1).max(500),
  plan: z.enum(['starter', 'team']),
}).refine((value) => value.plan !== 'starter' || value.seats <= 25, {
  path: ['seats'],
  message: 'The starter plan supports up to 25 seats.',
})

type Request = z.output<typeof requestSchema>
const form = reactive({ email: '', seats: '1', plan: 'starter' })
const errors = ref<Record<string, string>>({})
const status = ref('')

function validate(): Request | null {
  const result = requestSchema.safeParse(form)
  if (!result.success) {
    const next: Record<string, string> = {}
    for (const issue of result.error.issues) {
      const key = issue.path.join('.') || '_form'
      next[key] ??= issue.message
    }
    errors.value = next
    return null
  }
  errors.value = {}
  return result.data
}

async function submit() {
  const payload = validate()
  if (!payload) return
  status.value = 'Saving'
  await sendRequest(payload)
  status.value = 'Saved'
}

async function sendRequest(payload: Request) {
  // Use the application's API client here.
  await Promise.resolve(payload)
}
</script>

In the template, bind fields normally and render errors by their stable paths, such as errors.email and errors.seats. Use a visible label, aria-invalid, and aria-describedby for each control. A form-level issue can be rendered near the submit button. The example's asynchronous handler should be extended with pending state and error handling in production; schema success only means the input passed local rules, not that the network request will succeed.

The refinement attaches the plan rule to the seats path, which lets the UI place the issue beside the value the user can change. A rule that cannot be associated with one field should remain a form-level issue. Avoid parsing and then discarding the returned data: if the schema trims email or coerces seats, sending the original reactive object would bypass the normalized output. Send result.data so the API payload matches the contract.

Handle paths and type differences

Issue paths can include nested property names and array indices. A generic form renderer should format those paths consistently and avoid assuming every issue refers to a top-level string key. For a small known schema, explicit mapping is often clearer than a generic path engine. If multiple issues share a path, decide whether to show the first message, all messages, or a prioritized subset. The policy should avoid replacing a useful validation error with a less actionable one.

Coercion deserves tests around empty and malformed input. JavaScript numeric conversion has surprising cases, and an empty string can become zero under some coercion paths. If zero is invalid, the minimum rule will catch it, but a field may need a specific message that distinguishes missing input from a low value. Keep the raw form value when parsing fails so the user can correct it. Do not overwrite the field with an invalid parsed result.

Use schema input and output types intentionally. z.input describes values accepted before parsing; z.output describes the normalized values after success. A transform can make those types substantially different. If a server and browser share the schema package, compile-time inference reduces drift, while runtime parsing still verifies real values. The inferred type does not validate a network request by itself; the server must execute the parser.

Finally, test both branches. A valid form should return the normalized output shape. Invalid values should produce the expected paths and messages, including cross-field rules and nested values. A component test should verify how those issues are displayed and cleared as the form changes. This keeps the schema contract and user experience aligned.

Schema Validation with zod: Trade-offs and Interviews

One schema is not every validation layer

A schema can centralize deterministic data constraints, but it cannot answer every domain question. Whether an email address is already registered requires a server lookup. Whether a user may create a workspace depends on authentication and authorization. Whether a resource is still available can change between validation and persistence. Keep these checks at the service boundary and treat a successful client parse as a helpful local result, not permission to perform an operation.

Schemas with transformations create a raw-input and parsed-output distinction. Trimming, coercion, defaults, and transforms can change values. Use the parser's returned data rather than assuming the original object has been normalized. In TypeScript, choose z.input when modeling the editable representation and z.output when modeling accepted data. This prevents a form's string field from being mistaken for the numeric payload produced by parsing.

Async refinements need async parsing and explicit pending/error behavior. They also need stale-response handling: if a user changes the value while a uniqueness request is pending, an old result must not mark the new value valid or invalid. Often it is clearer to keep remote availability checks outside the pure schema and model them as a separate asynchronous state. Schemas are easiest to share when they remain deterministic and side-effect free.

Map errors into an accessible form

Zod issue paths are data, not user-facing copy. Map them to visible controls and provide understandable messages. For nested arrays, a path may look like an item index and field name; when rows can reorder, stable item identities are safer than keeping long-lived error state keyed only by an index. Clear or recompute errors when values change according to the chosen timing policy. Do not make the schema message the sole accessibility behavior: the template must still associate the message with its control and announce important changes.

A schema can report several issues at once. Showing all of them may overwhelm a small form, while showing only one can hide an independent correction the user could make immediately. Choose a predictable policy, such as first issue per field plus a summary count, and preserve that policy in tests. Cross-field issues should identify a field when the user can correct one, or appear in a form-level summary if the rule belongs to the object as a whole.

Testing and adoption decisions

Test schemas as pure functions: valid input returns the expected normalized output, invalid input returns paths and messages that matter to the UI, and boundary cases are covered. Then test the Vue component's interaction policy separately: when errors appear, whether they clear, what is sent after success, and how server failures are represented. A schema unit test does not prove the form is accessible, and a component snapshot does not prove the server validates its request.

Adopt a schema when rules are repeated, nested, or shared across client and server, or when parsed output types are valuable. Keep direct checks for a tiny one-off form if they are simpler and still tested. If a form library is added, understand whether it expects raw values or parsed values and how it integrates schema issues. Avoid maintaining the same constraint independently in a component, a schema, and an API route; duplicate rules eventually disagree.

In an interview, explain the editing representation, the schema parse boundary, and the output type. Describe how safeParse separates expected invalid input from exceptional failures, how issue paths map to controls, and why the server parses again. Mention where asynchronous business checks belong. This shows that schema validation is a contract and normalization step, not a replacement for Vue state, accessible presentation, or server trust boundaries.

Quiz

1. Which statement about Schema Validation with zod is accurate?

Question 1 options

2. When reviewing Schema Validation with zod, which point must hold?

Question 2 options

3. What does safeParse provide for expected invalid form input?

Question 3 options

4. When a Zod schema transforms or coerces values, what should the request use?

Question 4 options

Flashcards

Question

What is Schema Validation with zod?

Answer

Define typed schemas once and parse form data for both client and server.

Question

Schema Validation with zod: why it matters

Answer

A zod schema describes the shape and constraints of form data as a single source of truth.

Question

Schema Validation with zod: key detail

Answer

safeParse returns typed data or an issues array that maps directly to field errors.

Question

Schema Validation with zod: practical note

Answer

Sharing the schema with the server guarantees both sides validate identical rules.

Question

Which values should be sent after successful Zod parsing?

Answer

The parsed output, including any normalized or coerced values.

Revision Notes

Key Takeaways

  • 1. A zod schema describes the shape and constraints of form data as a single source of truth.
  • 2. safeParse returns typed data or an issues array that maps directly to field errors.
  • 3. Sharing the schema with the server guarantees both sides validate identical rules.

Interview Tips

  • • Explain Schema Validation with zod with one concrete example from real vue work, not a textbook restatement.
  • • Discuss the trade-off: when Schema Validation with zod helps and when a lighter approach wins.
  • • Place Schema Validation with zod inside the Forms and Validation phase — interviewers check that you see the progression, not isolated facts.

Cheat Sheet

Schema Validation with zod Cheat Sheet

Phase: Forms and Validation | Level: advanced

Key points:

  • A zod schema describes the shape and constraints of form data as a single source of truth.
  • safeParse returns typed data or an issues array that maps directly to field errors.
  • Sharing the schema with the server guarantees both sides validate identical rules.
  • Infer the TypeScript type from the schema so form state and schema never drift apart.

Tags: vue, forms, schemas

Continue Learning

Before You Start

  • No prerequisites listed.

Related Reading