ReactでZodを使ってフォームを検証するなら、React Hook Formの resolver にZodスキーマを渡す構成が標準です。ところが2026年10月時点では、手順どおり3つのパッケージを npm install するだけで依存解決エラーになります。10月19日公開の @hookform/resolvers 6.0.0 が原因です。加えて、2025年7月に出たZod 4ではエラーメッセージの指定方法が変わりました。Zod 3の書き方を残すと、日本語メッセージが黙って英語に戻ります。
この記事では zod 4.6.7・react-hook-form 7.91.0・@hookform/resolvers 5.9.2/6.0.0 を手元に入れ、tsc(strict)と実行で確かめた結果を載せます。Zod単体のインストールや基本APIは Zodの使い方入門 にまとめてあります。
まとめ
- 連携に要るのは react-hook-form・zod・@hookform/resolvers の3つです。2026年10月25日時点では resolvers 6.0.0 が npm で
ERESOLVEになるため、Zodだけを使うなら@hookform/[email protected]を指定します。zodResolver の実装は5.9.2と6.0.0で同一です。 useFormには型引数を書かず、resolver から推論させます。useForm<z.infer<typeof schema>>と書くと、.default()やz.coerceを含むスキーマで型エラーになります。- 確認用パスワードのようなオブジェクト全体の
.refine()は、他の項目に型の不一致があると実行されません。Zod 4のwhenで実行条件を指定できます。 - Zod 3の
required_errorとerrorMapは、Zod 4では警告もなく無視されます。全体の文言はz.config()のcustomErrorとロケールで決め、項目ごとのerrorで上書きします。 - zod 4.6.6未満には高深刻度の脆弱性(GHSA-mrhx-c7xc-m3w5)があり、3系に修正版はありません。サーバーとスキーマを共有するなら、配列には上限を付けて最初に置きます。
React Hook FormとZodをつなぐzodResolverの導入手順
react-hook-form は入力値の保持と再レンダーの制御を受け持ち、値が妥当かどうかの判断はZodに任せます。両者をつなぐのが @hookform/resolvers の zodResolver です。register や watch など react-hook-form そのものの使い方は React Hook Formの使い方とv7の書き方 を参照してください。
パッケージの版とresolvers 6.0.0のERESOLVE
2026年10月25日に npm registry で確認した最新版は次のとおりです(公開日はnpm上のUTC日付)。
| パッケージ | 最新版 | 公開日 | 注意点 |
|---|---|---|---|
| zod | 4.6.7 | 2026-10-12 | 4.6.6未満は脆弱性あり |
| react-hook-form | 7.91.0 | 2026-10-23 | v8は8.0.0-beta.5 |
| @hookform/resolvers | 6.0.0 | 2026-10-19 | npmでERESOLVE |
| @hookform/resolvers | 5.9.2 | 2026-10-17 | zodResolverは6.0.0と同一 |
resolvers 6.0.0 の破壊的変更は effect-ts 用 resolver の Effect v4 対応だけで、v6.0.0のリリースノート にもZodへの言及はありません。両版の zod/ ディレクトリを diff にかけても差分はゼロでした。問題は依存の宣言です。6.0.0 は任意の peer として effect@^4.0.0 を追加しましたが、同じく任意 peer の @typeschema/main を経由する依存関係が effect@^3 を要求しています。npm 7以降は任意 peer の範囲も検査するため、Effectを使わないプロジェクトでも衝突します。
空のディレクトリで npm install react-hook-form zod @hookform/resolvers react を実行すると、npm 11.17.0 では ERESOLVE unable to resolve dependency tree で止まります。この不具合は resolversのissue #882 で報告され、10月24日に PR #883 で修正されました。ただし10月25日時点で修正版は npm に出ていません。回避策と、それぞれの結果は次のとおりです。
# 1. 5.9.2 を明示する(Zodだけを使うならこれで十分)
npm install [email protected] [email protected] @hookform/[email protected]
# 2. 6.0.0 を使う場合は package.json に overrides を足す
# "overrides": { "effect": "^4.0.0" }
# 3. pnpm 11.15.1 は警告なしで 6.0.0 を入れられる
pnpm add react-hook-form zod @hookform/resolvers
1と2と3はいずれも成功し、--legacy-peer-deps を付けた場合も通りました。--legacy-peer-deps は他の peer の検査まで止めるので、使うのは最後の手段にしてください。6.0.1以降が出たら、npm view @hookform/resolvers version で版を確かめてから指定を外します。なお resolvers 5.0.1以降と6.0.0は peer に react-hook-form@^7.55.0 を宣言しており、7.54以下に留めているプロジェクトは react-hook-form を先に上げる必要があります。
サインアップフォームの実装
メールアドレス・パスワード・確認用パスワードを持つフォームを例にします。ReactとTypeScriptの開発環境を前提とし、送信先の /api/signup は別途実装してください。サーバー側でも受信データを検証する必要があります。スキーマを1つ定義し、zodResolver を通して useForm に渡します。
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
const signUpSchema = z
.object({
email: z.email({ error: "メールアドレスの形式が正しくありません" }),
password: z.string().min(8, { error: "8文字以上で入力してください" }),
confirm: z.string(),
})
.refine((v) => v.password === v.confirm, {
error: "パスワードが一致しません",
path: ["confirm"],
});
type SignUpValues = z.output<typeof signUpSchema>;
async function postSignUp(values: SignUpValues): Promise<void> {
const response = await fetch("/api/signup", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(values),
});
if (!response.ok) throw new Error("登録に失敗しました");
}
export function SignUpForm() {
const {
register,
handleSubmit,
setError,
formState: { errors, isSubmitting },
} = useForm({
resolver: zodResolver(signUpSchema),
mode: "onBlur",
defaultValues: { email: "", password: "", confirm: "" },
});
const onSubmit = handleSubmit(async (values) => {
try {
await postSignUp(values);
} catch {
setError("root.server", { message: "登録に失敗しました。再度お試しください" });
}
});
return (
<form onSubmit={onSubmit} noValidate>
<label>メールアドレス<input type="email" {...register("email")} /></label>
{errors.email && <p role="alert">{errors.email.message}</p>}
<label>パスワード<input type="password" {...register("password")} /></label>
{errors.password && <p role="alert">{errors.password.message}</p>}
<label>確認用パスワード<input type="password" {...register("confirm")} /></label>
{errors.confirm && <p role="alert">{errors.confirm.message}</p>}
{errors.root?.server && <p role="alert">{errors.root.server.message}</p>}
<button type="submit" disabled={isSubmitting}>登録する</button>
</form>
);
}
このコードは、送信失敗を setError("root.server", ...) で画面に出す処理まで含めて、TypeScript 7.0.2 の strict: true で tsc --noEmit を通ります。メールアドレスにはトップレベルの z.email() を使い、メッセージは message ではなく error で渡します。mode: "onBlur" はフォーカスが外れた時点で検証する指定です。既定の onSubmit のままでは、送信ボタンを押すまでエラーが出ません。
zodResolverが返すエラーオブジェクトとオプション
zodResolver が返す関数を直接呼ぶと、errors に何が入るかを確認できます。上のスキーマに { email: "x", password: "123", confirm: "999" } を渡した結果が次です。
// zod 4.6.7 / @hookform/resolvers 5.9.2
{
"values": {},
"errors": {
"email": { "message": "メールアドレスの形式が正しくありません", "type": "invalid_format" },
"password": { "message": "8文字以上で入力してください", "type": "too_small" },
"confirm": { "message": "パスワードが一致しません", "type": "custom" }
}
}
type にはZodのissueコードがそのまま入ります。同じメール検証をZod 3.25.76のスキーマで行うと invalid_string でした。errors.email?.type で分岐しているコードは、Zod 4へ上げた時点で条件が外れます。
zodResolver は zodResolver(schema, schemaOptions, resolverOptions) の3引数を取ります(resolversのREADME)。第3引数で効くのは次の2つです。
raw: true… 検証は行いますが、送信データは変換前の入力値になります。z.coerce.number()の項目に"20"を渡すと、既定では数値の20が届き、raw: trueでは文字列の"20"のままでした。.default()の補完も行われません。mode: "sync"… 既定のparseAsyncではなくparseで検証します。スキーマに非同期の.refine()が1つでもあると$ZodAsyncError: Encountered Promise during synchronous parseで例外になるため、サーバー問い合わせを含むフォームでは指定しません。
文字列で届くフォーム入力をZodスキーマで受ける型設計
HTMLの入力部品が返す値は、type="number" であっても文字列です。この性質を無視すると、型は通るのに実行時に落ちる、あるいはその逆が起きます。
数値入力とz.coerceの空文字
年齢や個数には z.coerce.number() を使います。ただし変換が先に走るため、z.coerce.number().int().min(1, { error: "1以上で入力してください" }) に空文字を渡すと、Number("") の結果である0として扱われ、「1以上で入力してください」が返ります。未入力が「0が入力された」と区別できなくなるわけです。
未入力と0を分けたい項目は、z.union([z.literal(""), z.coerce.number().min(1)]) のように空文字を別扱いにします。この形では空文字と "5" が通り、"0" が不合格になります。文字列の任意項目も同じで、z.string().min(3).or(z.literal("")) と書けば空欄を許しつつ、入力があるときだけ3文字以上を求められます。
useFormの型推論と入力型・出力型の指定
z.coerce や .default() を含むスキーマは、検証前の型と検証後の型が一致しません。ここで useForm<z.infer<typeof schema>> と出力型を渡すと、tsc 7.0.2 は TS2322: Type 'Resolver<{ nickname?: string | undefined; age: unknown; }, ...>' is not assignable to ... で止まります。現行の zodResolver は入力型と出力型の両方を型として持っているので、型引数は書かずに推論させるのが最短です。
const profileSchema = z.object({
nickname: z.string().default("ゲスト"),
age: z.coerce.number().int().min(1, { error: "1以上で入力してください" }),
});
// 型引数を書かない:resolver から入力型と出力型が推論される
const { register, handleSubmit } = useForm({ resolver: zodResolver(profileSchema) });
const onSubmit = handleSubmit((data) => {
const age: number = data.age; // 出力型(数値)で届く
const nickname: string = data.nickname; // default 適用後なので string
});
型を明示したい場合は useForm<z.input<typeof s>, unknown, z.output<typeof s>> の3引数で書きます。これも tsc を通ります。Zod 4の z.coerce.number() は入力型が unknown になる点も覚えておくと、register("age") まわりの型で迷いません。
refineとsuperRefineによるカスタムバリデーション
「パスワードと確認用が一致する」「開始が終了より前」のように、1項目の制約では書けない条件はスキーマ全体への検証で表します。Zod 4では .refine()・.superRefine()・.check() の3つが使えます。
複数フィールドをまたぐ検証とpath指定
オブジェクトに付けた .refine() では path が欠かせません。省略すると issue の path が空配列になり、zodResolver の戻り値は { "": { message: "不一致", type: "custom" } } でした。空文字のキーに入るため errors.confirm は undefined のままで、確認用の欄には何も表示されません。エラーを出したい側のフィールド名を path: ["confirm"] と書けば、errors.confirm.message に届きます。
他の項目が型エラーだとrefineが実行されない問題とwhen
フォームで最も気づきにくいのがこの挙動です。Zodは、型の不一致のような継続できない issue がすでにあると、refine を実行しません(Zod公式のAPIリファレンス の when の項)。zod 4.6.7 で境界を確かめると、次のように分かれました。
| 他の項目の状態 | issueコード | 一致確認のrefine |
|---|---|---|
| メールが空文字・形式違い | invalid_format | 実行される |
| メールの値が undefined | invalid_type | 実行されない |
| 数値項目に文字列が入る | invalid_type | 実行されない |
z.coerce.number() に "abc" |
invalid_type | 実行されない |
つまり z.number() の項目を valueAsNumber なしで register していたり、defaultValues を渡さず未入力の項目が undefined のままだったりすると、パスワードの不一致は他の欄を直すまで表示されません。利用者から見ると、エラーが後から1つずつ増えていく画面になります。
対処は when で実行条件を書くことです。比較に使う2項目だけを取り出して検証し、通っていれば実行させます。公式のAPIリファレンスと同じ pick() の形で、他の項目の状態には左右されなくなります。
const base = z.object({
email: z.email({ error: "メールアドレスの形式が正しくありません" }),
age: z.number({ error: "年齢は数値で入力してください" }),
password: z.string().min(8, { error: "8文字以上で入力してください" }),
confirm: z.string(),
});
export const signUpSchema = base.refine((v) => v.password === v.confirm, {
error: "パスワードが一致しません",
path: ["confirm"],
// password と confirm が検証を通れば、他の項目が不正でも実行する
when(payload) {
return base.pick({ password: true, confirm: true }).safeParse(payload.value).success;
},
});
{ email: "x", age: "20", password: "12345678", confirm: "9" } を渡すと、when なしでは email と age の2件しか返りませんが、when ありでは confirm の「パスワードが一致しません」まで同時に返りました。pick() は min(8) も引き継ぐので、パスワードが8文字未満のあいだは一致確認も走りません。短くても一致を確かめたいなら、z.object({ password: z.string(), confirm: z.string() }) のように型だけを見るスキーマを when に使います。なお min のような通常の失敗は継続できる issue なので、後ろの refine も実行されます。止まるのは型の不一致など継続できない issue があるときで、これは項目単位の refine でも同じです。フォームで目立つのは、他の項目の型エラーに巻き込まれるオブジェクト全体の refine です。
superRefineのaddIssueによる複数エラーの登録
1回の検証で複数の issue を積むなら .superRefine() の ctx.addIssue() を使います。code を省くと "custom" が補われます。
export const shiftSchema = z
.object({ start: z.number(), end: z.number() })
.superRefine((v, ctx) => {
if (v.start >= v.end) {
ctx.addIssue({ code: "custom", message: "終了は開始より後の時刻にしてください", path: ["end"] });
}
if (v.end > 22) {
ctx.addIssue({ code: "custom", message: "終了は22時までにしてください", path: ["end"] });
}
});
同じ end に2件積んでも、react-hook-form の既定(criteriaMode: "firstError")では errors.end に最初の1件しか入りません。{ start: 23.5, end: 23 } を criteriaMode: "all" で検証すると、errors.end.types.custom に2つのメッセージが配列で入りました。条件を一覧で見せたいパスワード規則などは useForm({ criteriaMode: "all" }) にして types を描画します。
.superRefine() の位置づけは公式の中でも割れています。zod.dev/packages/zod のコード例には // deprecated, use .check() とコメントがありますが、4.6.7 の型定義の superRefine に @deprecated は無く、エディタで取り消し線は出ません。APIリファレンスも現役のAPIとして説明し、.check() は「より低レベルで、一般に複雑」と位置づけています。この記事で検証した4.6.7では .superRefine() を利用できますが、公式パッケージページの移行案内に合わせるなら .check() を選びます。ただし、削除時期はその案内に示されていません。
export const slotSchema = z
.object({ start: z.number(), end: z.number() })
.check((ctx) => {
if (ctx.value.start >= ctx.value.end) {
ctx.issues.push({
code: "custom",
message: "終了は開始より後の時刻にしてください",
input: ctx.value,
path: ["end"],
});
}
});
.check() は値を ctx.value で受け、ctx.issues.push() で issue を足します。input は必須で、省くと tsc 7.0.2 は TS2741: Property 'input' is missing を出します。コンテキストのキーを列挙すると、zod 3.25.76 の superRefine は addIssue, path、4.6.7 の superRefine は value, issues, addIssue、check は value, issues でした。Zod 4にはどちらにも ctx.path が無いので、ネストしたスキーマで ctx.path から位置を組み立てていたコードは、issueごとに path を書く形へ直します。
Zodのエラーメッセージの日本語化とcustomErrorによる一括指定
z.locales.jaの出力をそのまま使える項目
Zod 4には翻訳済みのロケールが同梱されています。4.6.7 で z.locales のキーは63件あり、z.config(z.locales.ja()) の1行で日本語になります。ただし文面の質は検証の種類で差があります。
| 検証 | z.locales.ja() の出力 | 画面に出せるか |
|---|---|---|
| z.email() | 無効なメールアドレス | 出せる |
| z.string() に undefined | 無効な入力: stringが期待されましたが… | 差し替え |
| z.string().min(8) | 小さすぎる値: stringは8文字以上… | 差し替え |
| z.number() に文字列 | 無効な入力: 数値が期待されましたが… | 差し替え |
必須・文字数・型違いの3種は「string」という型名が出る開発者向けの文面です。それでもロケールを入れておけば、個別指定がない箇所にも日本語の既定メッセージが使われます。ただし、stringなどの型名は残り、優先順位の高い設定で指定した英語メッセージも置き換わりません。
Zod 3のグローバルエラーマップからcustomErrorへの移行
Zod 3で z.setErrorMap() に書いていた全体のエラー文は、Zod 4では z.config({ customError }) に移ります(Zod公式のエラー文のカスタマイズ)。4.6.7 にも z.setErrorMap() は残っていますが、@deprecated 付きで、中身は customError を設定するだけです。ロケールの文面が開発者向けだった種類も、ここでまとめて差し替えられます。次のコードは文字数不足と未入力の2種を扱います。
import * as z from "zod";
z.config(z.locales.ja());
z.config({
customError: (issue) => {
if (issue.code === "too_small" && issue.origin === "string") {
return `${issue.minimum}文字以上で入力してください`;
}
if (issue.code === "invalid_type" && issue.input === undefined) {
return "入力してください";
}
return undefined; // それ以外はロケール(日本語の既定文)に任せる
},
});
この設定で min(3) の未達は「3文字以上で入力してください」、未入力は「入力してください」、メール形式はロケールの「無効なメールアドレス」になりました。数値項目に文字列が入った場合のような他の型違いは、条件を足さない限りロケールの文面が残ります。項目に error を書いた箇所はその文が優先されます。優先順位は公式の説明どおり「チェックごとの error」→「スキーマごとの error」→「parse時に渡す error」→「customError」→「ロケール」で、customError が undefined を返すと次へ回ります。z.config() は呼ぶたびに設定を合成するので、ロケールと customError は別々の呼び出しで共存します。
Zod 3からZod 4へ上げたときフォームで壊れる箇所
黙って無視されるrequired_errorとerrorMap
危ないのは、実行時に例外が出ず、表示だけが変わる変更です。TypeScriptで型チェックしていれば検出できます。
const schema = z.string({ required_error: "必須です", invalid_type_error: "文字列で" });
schema.safeParse(undefined).error.issues[0].message;
// zod 3.25.76 -> "必須です"
// zod 4.6.7 -> "Invalid input: expected string, received undefined"
Zod 4は required_error・invalid_type_error・errorMap を受け取っても例外を投げず、既定の英語メッセージを返します。日本語の必須メッセージを全項目に書いていたフォームでも、Zod 4へ上げた瞬間に未入力の文言だけが英語に戻ります。型チェックにかければ TS2769 で「’required_error’ does not exist in type ‘$ZodStringParams’」と名指しされるので、危ないのは JavaScript のまま書いているプロジェクトや、CIで tsc を回していないプロジェクトです。移行時はまず3つのキーを grep し、error に書き換えます。関数を渡せば未入力と型違いを1か所で分けられます。
z.string({
error: (issue) => (issue.input === undefined ? "未入力です" : "文字列で入力してください"),
});
Zod 4移行時の非推奨APIと互換性変更
| Zod 3の書き方 | zod 4.6.7での扱い | 置き換え先 |
|---|---|---|
| required_error など | 無視される | error |
| z.setErrorMap() | 動くが非推奨 | z.config({ customError }) |
| z.string().email() | 動くが非推奨 | z.email() |
| error.errors | undefined | error.issues |
| error.format() / flatten() | 非推奨 | z.treeifyError() / z.flattenError() |
| superRefine の ctx.path | 存在しない | issueごとにpathを書く |
| z.record(z.string()) | 実行は通る・型は不可 | キーと値の2引数 |
| z.string().uuid() | z.uuid()はRFC準拠で厳格 | 緩い判定は z.guid() |
実行時に落ちるのは error.errors だけで、error.errors.map(...) は TypeError になります。z.record() の単一引数は実行こそ成功しますが、tsc では TS2554: Expected 2-3 arguments, but got 1 です。error.flatten() は型定義の @deprecated が z.treeifyError() を案内していますが、{ formErrors, fieldErrors } の形を保ちたいなら非推奨でない z.flattenError() が対応します。UUIDは 12345678-1234-f234-a234-123456789abc のような版番号の桁が規格外の値を、zod 3.25.76 は通し、z.uuid() は弾き、z.guid() は通しました。
Zod 3に留まる判断と脆弱性
以前は「react-hook-form を7.55.0以上に上げられないなら、resolvers 4系とZod 3に留まる」が現実的な選択でした。2026年10月1日(UTC)に公開された GHSA-mrhx-c7xc-m3w5(CVE-2023-54404・CVSSはv3.1で7.5、v4.0で8.2)で事情が変わっています。GitHub Advisory Databaseは影響範囲を zod 4.6.6未満、修正の入った最初の版を4.6.6としています。一方でCNAの VulnCheckの勧告 は、上限のない配列・Set・Mapについて4.6.7も影響対象とし、この場合を直したリリースは無いとしています(後述)。3系に修正版は無く、zod 3.25.76 を入れたプロジェクトで npm audit を実行すると high が出て、npm audit fix --force は4.6.7への破壊的な更新を提案します。サーバー側でも同じスキーマで配列を検証しているなら、Zod 3に留まる選択は修正のない脆弱性を抱えたままにすることを意味します。react-hook-form を7.55.0以上へ上げ、Zod 4へ移る方を優先してください。
Zod 3とZod 4の検証速度と配列の上限
zod 3.25.76とzod 4.6.7の速度の実測
メール・文字列(1〜50文字)・整数(0〜120)・文字列配列(最大5件)の4項目のスキーマを両版で組み、Node v26.5.0(x64)でウォームアップ2,000回のあと safeParse を20万回計測しました。
| 試行 | zod 3.25.76 | zod 4.6.7 |
|---|---|---|
| 1回目 | 771ms | 508ms |
| 2回目 | 802ms | 520ms |
| 3回目 | 767ms | 502ms |
平均ではZod 4が約1.53倍速く、1回あたり約2.55マイクロ秒、Zod 3は約3.90マイクロ秒です。Node.js上のスキーマ単体の計測なので、ブラウザでの描画を含む体感速度までは測っていません。この計測での1回あたりの差は約1.35マイクロ秒で、この記事ではZod 4を選ぶ理由を速度ではなくエラーAPIの整理とロケールの同梱に置きます。速度とバンドルサイズが本当に効く規模なら、ValibotとZodの違い を比べる段階です。
上限のない配列で100万件のissueが積まれる問題
フォームとAPIで同じスキーマを共有すると、ブラウザの入力欄では起きない大きな入力がサーバーに届きます。GHSA-mrhx-c7xc-m3w5 の修正(zodのPR #6652)は、上限を他の検証より先に判定するもので、上限の無い配列の挙動は変えていません。zod 4.6.7 に100万要素の不正な配列を渡した結果が次です。
| スキーマ | issue数 | 時間 | ヒープ増 |
|---|---|---|---|
| z.array(z.string()) | 1,000,000 | 1,745ms | 約319MB |
| .refine(…).max(100) | 1,000,001 | 1,172ms | 約322MB |
| .max(100) | 1 | 2ms | ほぼ0 |
| .max(100).refine(…) | 1 | 1ms未満 | ほぼ0 |
4.6.7 でも、上限が無いか、上限より前に refine を置いた配列は1要素ごとに issue を積みます。共有するスキーマでは、配列に .max() を付け、他の検証より前に置いてください。
// 上限を他のチェックより前に置く
export const tagsSchema = z.array(z.string().max(30)).max(20).refine((a) => new Set(a).size === a.length, {
error: "同じタグが重複しています",
});
この形なら100万要素の配列も issue 1件で返ります。リクエスト本文のサイズ上限はサーバー側で別に設けます。
よくある質問
Zod 4でz.string().email()は使えなくなりましたか?
使えます。zod 4.6.7 で z.string().email().safeParse("[email protected]") は成功し、警告も出ません。型定義に @deprecated が付いているのでエディタでは取り消し線が出ます。新しく書くコードはトップレベルの z.email() を使ってください。
zodResolverはZod 3やZod Miniのスキーマでも動きますか?
動きます。resolvers 5.9.2 と 6.0.0 の peer は zod: ^3.25.0 || ^4.0.0 です。zod/mini で組んだスキーマを渡しても、invalid_format や too_small を含むエラーが正しく返りました。Zod 3のスキーマでは type の値が変わる(メール形式は invalid_string)ので、混在させずどちらかに揃えます。
React NativeでもZodとReact Hook Formは使えますか?
使えます。スキーマと zodResolver はWebと同じで、違うのは入力部品のつなぎ方です。React Hook Formの公式ガイド は、React Native の TextInput を Controller で包み、onChangeText から値を渡す方法を示しています。Controller の扱いは MUIとReact Hook Formの連携方法 でも詳しく扱っています。アプリ全体の設計から相談したい場合は Flutter / React Native開発 の支援もあります。
React Hook Form v8のベータでもzodResolverは使えますか?
npm の通常の手順では入りません。resolvers 5.9.2 の peer は react-hook-form@^7.55.0 なので、8.0.0-beta.5 と一緒に入れると ERESOLVE になります。--legacy-peer-deps で入れれば zodResolver 単体の呼び出しは動きましたが、公式にサポートされた組み合わせではありません。v8は useFieldArray の識別子が id から key に変わるなどの破壊的変更を含むので(v7からv8への移行ガイド)、本番は7.91.0に留めます。
TanStack FormなどReact Hook Form以外でもZodは使えますか?
使えます。Zod 4のスキーマは Standard Schema に準拠しているので、同じ仕様に対応したライブラリならアダプタなしで受け付けます。React向けでは TanStack Formでのzod検証 が代表例です。サーバー側では resolver を介さず schema.safeParse(body) を呼び、z.flattenError() で項目ごとのメッセージ配列にすると、クライアントの表示ロジックを流用できます。