safe-action-validation-errors

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

next-safe-action Validation Errors

next-safe-action 验证错误处理

Two Sources of Validation Errors

两类验证错误来源

  1. Schema validation — automatic when input doesn't match
    .inputSchema()
  2. Manual validation — via
    returnValidationErrors()
    in server code (e.g., "email already taken")
Both produce the same error structure on the client.
  1. Schema验证——当输入不符合
    .inputSchema()
    时自动触发
  2. 手动验证——通过服务端代码中的
    returnValidationErrors()
    实现(例如“邮箱已被占用”)
这两类错误在客户端会生成相同的错误结构。

Default Error Shape (Formatted)

默认错误结构(格式化形式)

Mirrors the schema structure with
_errors
arrays at each level:
ts
// For schema: z.object({ email: z.string().email(), address: z.object({ city: z.string() }) })
{
  _errors: ["Form-level error"],                    // root errors
  email: { _errors: ["Invalid email address"] },    // field errors
  address: {
    _errors: ["Address section error"],
    city: { _errors: ["City is required"] },        // nested field errors
  },
}
镜像Schema结构,每个层级都带有
_errors
数组:
ts
// For schema: z.object({ email: z.string().email(), address: z.object({ city: z.string() }) })
{
  _errors: ["Form-level error"],                    // root errors
  email: { _errors: ["Invalid email address"] },    // field errors
  address: {
    _errors: ["Address section error"],
    city: { _errors: ["City is required"] },        // nested field errors
  },
}

returnValidationErrors

returnValidationErrors

Throws a
ActionServerValidationError
that the framework catches and returns as
result.validationErrors
. It never returns — it always throws.
ts
"use server";

import { z } from "zod";
import { returnValidationErrors } from "next-safe-action";
import { actionClient } from "@/lib/safe-action";

const registerSchema = z.object({
  email: z.string().email(),
  username: z.string().min(3),
});

export const register = actionClient
  .inputSchema(registerSchema)
  .action(async ({ parsedInput }) => {
    // Check business rules after schema validation passes
    const existingUser = await db.user.findByEmail(parsedInput.email);
    if (existingUser) {
      returnValidationErrors(registerSchema, {
        email: { _errors: ["This email is already registered"] },
      });
    }

    const existingUsername = await db.user.findByUsername(parsedInput.username);
    if (existingUsername) {
      returnValidationErrors(registerSchema, {
        username: { _errors: ["This username is taken"] },
      });
    }

    // Both checks passed — create the user
    const user = await db.user.create(parsedInput);
    return { id: user.id };
  });
会抛出一个
ActionServerValidationError
,框架会捕获该错误并将其作为
result.validationErrors
返回。该方法不会返回值——它始终会抛出错误。
ts
"use server";

import { z } from "zod";
import { returnValidationErrors } from "next-safe-action";
import { actionClient } from "@/lib/safe-action";

const registerSchema = z.object({
  email: z.string().email(),
  username: z.string().min(3),
});

export const register = actionClient
  .inputSchema(registerSchema)
  .action(async ({ parsedInput }) => {
    // Check business rules after schema validation passes
    const existingUser = await db.user.findByEmail(parsedInput.email);
    if (existingUser) {
      returnValidationErrors(registerSchema, {
        email: { _errors: ["This email is already registered"] },
      });
    }

    const existingUsername = await db.user.findByUsername(parsedInput.username);
    if (existingUsername) {
      returnValidationErrors(registerSchema, {
        username: { _errors: ["This username is taken"] },
      });
    }

    // Both checks passed — create the user
    const user = await db.user.create(parsedInput);
    return { id: user.id };
  });

Root-Level Errors

根层级错误

Use
_errors
at the top level for form-wide errors:
ts
returnValidationErrors(schema, {
  _errors: ["You can only create 5 posts per day"],
});
在顶层使用
_errors
来定义表单级别的全局错误:
ts
returnValidationErrors(schema, {
  _errors: ["You can only create 5 posts per day"],
});

Supporting Docs

相关文档

  • Custom validation errors and returnValidationErrors patterns
  • Formatted vs flattened shapes, per-action override
  • Custom validation errors and returnValidationErrors patterns
  • Formatted vs flattened shapes, per-action override

Displaying Validation Errors

验证错误的展示

tsx
// Formatted shape (default)
{result.validationErrors?.email?._errors?.map((error) => (
  <p key={error} className="text-red-500">{error}</p>
))}

// Root-level errors
{result.validationErrors?._errors?.map((error) => (
  <p key={error} className="text-red-500">{error}</p>
))}
tsx
// Flattened shape
{result.validationErrors?.fieldErrors?.email?.map((error) => (
  <p key={error} className="text-red-500">{error}</p>
))}

// Form-level errors (flattened)
{result.validationErrors?.formErrors?.map((error) => (
  <p key={error} className="text-red-500">{error}</p>
))}
tsx
// Formatted shape (default)
{result.validationErrors?.email?._errors?.map((error) => (
  <p key={error} className="text-red-500">{error}</p>
))}

// Root-level errors
{result.validationErrors?._errors?.map((error) => (
  <p key={error} className="text-red-500">{error}</p>
))}
tsx
// Flattened shape
{result.validationErrors?.fieldErrors?.email?.map((error) => (
  <p key={error} className="text-red-500">{error}</p>
))}

// Form-level errors (flattened)
{result.validationErrors?.formErrors?.map((error) => (
  <p key={error} className="text-red-500">{error}</p>
))}