Standard Schemaは、Zod・Valibot・ArkTypeなどのスキーマ検証ライブラリが共通で実装するTypeScriptのインターフェース仕様です。ライブラリそのものではなく、スキーマオブジェクトに ~standard という1つのプロパティを持たせる取り決めだけで成り立っています。フォームライブラリやAPIフレームワークは、この1つのプロパティを読むだけで、利用者がどの検証ライブラリで書いたスキーマでも受け取れます。
この記事では、仕様パッケージ @standard-schema/spec 1.1.0 の型定義をもとに中身を整理し、受け取る側と実装する側のコードを示します。主要な検証処理は Node.js 26.5.0・TypeScript 7.0.2 の検証用コードで確認しており、tRPC・Hono・React Hook Form・TanStack Formに同じスキーマを渡したときの挙動の違いも載せています。
まとめ:Standard Schemaの要点
- 正体:
StandardSchemaV1というTypeScriptのインターフェースです。実行時のコードは含まず、スキーマに~standard.validate()と型情報を持たせる約束事だけを定めています。 - 何が楽になるか:ツール側はライブラリごとのアダプターを書かずに済みます。公式サイトの対応一覧では、実装しているスキーマライブラリが33、受け取るツールが64あります(2026年10月8日時点)。
- 主な対応版:Zod 3.24.0以降、Valibot v1.0以降、ArkType v2.0以降、yup v1.7.0以降、joi v18.0.0以降。
- 最新版:
@standard-schema/spec1.1.0(2025年12月15日公開)。JSON Schemaへの変換を定めたStandard JSON Schemaが同じパッケージに入りました。 - 受け取る側の注意:
validate()は同期でもPromiseでも返せる仕様です。yupは常にPromiseを返すため、同期前提のコードは例外になります。 - 標準化されないもの:エラーのコード(
too_smallなど)、デフォルト値、メタデータは対象外です。エラーはmessageとpathだけに揃います。 - 依存の置き場所:仕様パッケージは型だけですが、公開APIで受け取るライブラリは
devDependenciesでなくdependenciesに入れるよう公式FAQが求めています。
~standardプロパティの中身:version・vendor・validate・types
仕様の本体は StandardSchemaV1 インターフェースです。1.1.0では共通部分が StandardTypedV1 に切り出されましたが、スキーマが持つべき項目は次の4つで変わっていません。
// @standard-schema/spec 1.1.0 の定義を要約
interface StandardSchemaV1<Input = unknown, Output = Input> {
readonly "~standard": {
readonly version: 1; // 仕様のバージョン番号
readonly vendor: string; // "zod" "valibot" "arktype" など
readonly validate: (
value: unknown,
options?: { readonly libraryOptions?: Record<string, unknown> }
) => Result<Output> | Promise<Result<Output>>;
readonly types?: { readonly input: Input; readonly output: Output };
};
}
type Result<Output> =
| { readonly value: Output; readonly issues?: undefined } // 成功
| { readonly issues: ReadonlyArray<Issue> }; // 失敗
interface Issue {
readonly message: string;
readonly path?: ReadonlyArray<PropertyKey | { readonly key: PropertyKey }>;
}
成功と失敗は issues の有無で判定します。仕様で共通化されるエラー項目は、必須の message と任意の path です。実際のエラーには、ライブラリ固有の項目が含まれることもあります。
types は型推論のための任意項目で、実行時の値を用意する必要はありません。Zod 4.6.5のオブジェクトスキーマで schema["~standard"].types を出力すると undefined でした。型を取り出すときは、仕様が用意している InferInput と InferOutput を使います。
import type { StandardSchemaV1 } from "@standard-schema/spec";
import * as z from "zod";
const User = z.object({ name: z.string().min(1), age: z.string().transform(Number) });
type In = StandardSchemaV1.InferInput<typeof User>; // { name: string; age: string }
type Out = StandardSchemaV1.InferOutput<typeof User>; // { name: string; age: number }
キー名の先頭がチルダ(~)なのは、エディタの補完候補で一番下に並べるためです。公式FAQによると、~ はASCIIで英数字より後ろに並ぶ数少ない文字で、アンダースコア始まりにすると逆に英字のメソッドより上に出てしまいます。シンボルをキーにしなかった理由も同じFAQにあり、TypeScriptではインラインの Symbol キーが単なる symbol 型に潰れて、他のシンボルプロパティと衝突するためと説明されています。
@standard-schema/spec 1.1.0の変更点とStandard JSON Schema
npmの公開履歴では、1.0.0が2025年1月、1.1.0が2025年12月15日です。GitHubリポジトリは2024年4月に作られ、1.0.0のベータ版は2024年9月から公開されていました。1.1.0で入った変更は次の3つで、既存の StandardSchemaV1 を実装したライブラリはそのまま互換です。
| 変更 | 中身 |
|---|---|
StandardTypedV1 |
version・vendor・typesだけを持つ基底型 |
validate の第2引数 |
libraryOptions でライブラリ固有の設定を渡す |
StandardJSONSchemaV1 |
JSON Schemaへの変換メソッドを定める別仕様 |
Standard JSON Schemaは、検証ではなく変換の仕様です。~standard.jsonSchema.input() と output() に target を渡すとJSON Schemaのオブジェクトが返ります。target には "draft-2020-12"・"draft-07"・"openapi-3.0" が定義され、前の2つへの対応が強く推奨されています。対応版はZod v4.2以降、ArkType v2.1.28、Valibotは @valibot/to-json-schema v1.5以降の toStandardJsonSchema() 経由です。
import * as z from "zod";
const Form = z.object({ age: z.string().transform(Number) });
Form["~standard"].jsonSchema.input({ target: "draft-2020-12" });
// { "$schema": "https://json-schema.org/draft/2020-12/schema",
// "type": "object", "properties": { "age": { "type": "string" } }, "required": ["age"] }
Form["~standard"].jsonSchema.output({ target: "draft-07" });
// Error: Transforms cannot be represented in JSON Schema
入力用と出力用が分かれているのは、transform で文字列を数値に変えるように、検証の前後で型が変わるスキーマがあるためです。Zod 4.6.5では出力側の変換が例外になりました。仕様も、変換できないスキーマでは例外を投げてよいとしているので、受け取る側は try で囲む必要があります。JSON Schema自体の書き方はJSON Schemaとは?JSONの構造を検証する書き方とDraft 2020-12の実装を実装者目線で解説で扱っています。
対応ライブラリと必要なバージョン
公式サイトの対応一覧から、利用者の多いライブラリを抜き出しました。一覧の版より古い版では ~standard プロパティが無いため、ツールに渡すと「対応していないスキーマ」として扱われます。
| ライブラリ | Standard Schema | Standard JSON Schema |
|---|---|---|
| Zod | 3.24.0+ | v4.2+ |
| Valibot | v1.0+ | v1.2(変換パッケージ経由) |
| ArkType | v2.0+ | v2.1.28 |
| Effect Schema | v3.13.0+(アダプター経由) | 一覧に無し |
| yup | v1.7.0+ | 一覧に無し |
| joi | v18.0.0+ | 一覧に無し |
| typia | v9.2.0+ | 一覧に無し |
| VineJS | v4.0.0+ | v4.3.0+ |
| Mongoose | v9.7.0+ | 一覧に無し |
同じ入力を4ライブラリで検証すると、vendor はそれぞれ "zod"・"valibot"・"arktype"・"yup" になり、version はすべて 1 でした。エラーの並び順と文面はライブラリごとに異なります。たとえば空文字の name に対して、Zodは「Too small: expected string to have >=1 characters」、ArkTypeは「name must be non-empty」を返しました。仕様が揃えるのは結果の形までで、文面は揃いません。各ライブラリの書き方はZodの使い方入門|npmでのインストールとz.infer・safeParse・Zod 4.6の変更点、Valibotとは?Zodとの違いとpipe構文の使い方をv1系の実コードで解説、ArkTypeとは?TypeScript実行時型検証の使い方とZodとの違いを参照してください。
受け取る側の実装:任意のスキーマで検証する関数
非同期を含めて受け取るstandardValidate
ツールやライブラリを作る側は、引数の型を StandardSchemaV1 にしておけば、利用者がどのライブラリで書いたスキーマでも受け取れます。戻り値の型は InferOutput で取り出します。
import type { StandardSchemaV1 } from "@standard-schema/spec";
import { SchemaError } from "@standard-schema/utils";
import * as z from "zod";
export async function standardValidate<T extends StandardSchemaV1>(
schema: T,
input: unknown,
): Promise<StandardSchemaV1.InferOutput<T>> {
let result = schema["~standard"].validate(input);
if (result instanceof Promise) result = await result;
if (result.issues) throw new SchemaError(result.issues);
return result.value;
}
// Zod・Valibot・ArkType・yup のどれを渡しても同じ関数で検証できる
await standardValidate(z.object({ age: z.string().transform(Number) }), { age: "42" }); // { age: 42 }
SchemaError は公式のユーティリティ @standard-schema/utils(0.3.0)が提供する例外クラスで、issues プロパティに全エラーを保持します。メッセージには先頭のエラーの文面が入ります。
同期だけ受け付ける場合のPromise判定
validate() の戻り値は同期の結果か Promise のどちらかです。同期でしか処理できない場所(フォームの入力ごとの検証など)では、公式FAQのとおり Promise を検出して例外にします。
import type { StandardSchemaV1 } from "@standard-schema/spec";
function validateSync<T extends StandardSchemaV1>(schema: T, data: unknown): StandardSchemaV1.InferOutput<T> {
const result = schema["~standard"].validate(data);
if (result instanceof Promise) {
throw new TypeError("Schema validation must be synchronous");
}
if (result.issues) throw new Error(result.issues.map((i) => i.message).join(", "));
return result.value;
}
どのスキーマが Promise を返すかはライブラリによって違います。検証環境では、Zod・Valibot・ArkTypeは通常のスキーマで同期の結果を返し、yup 1.7.1は単純な yup.string() でも常に Promise を返しました。Zodでも refine に非同期関数を渡すと Promise になります。同期専用のAPIを公開するなら、yupを渡されたときに例外になることをドキュメントに書いておく必要があります。
pathの扱い:getDotPathでの文字列化
エラーの path は、キーそのものが並ぶ場合と { key } を持つオブジェクトが並ぶ場合の両方が仕様で許されています。Zod・ArkType・yupは前者、Valibotは後者でした。そのため issue.path.join(".") と書くと、Valibotのスキーマでは "[object Object].[object Object]" になります。
import { getDotPath } from "@standard-schema/utils";
const result = { issues: [{ message: "都市名を入力してください", path: ["address", { key: "city" }] }] };
for (const issue of result.issues) {
const field = getDotPath(issue); // "address.city" のような文字列。表せない場合は null
console.log(field ?? "(フォーム全体)", issue.message);
}
getDotPath は両方の形を吸収します。パスが無い場合や、キーに文字列・数値以外(シンボル)が含まれる場合は null を返すので、フォーム全体のエラーとして扱います。
自作スキーマの実装:~standardプロパティの追加
自作の検証関数をStandard Schema対応にするには、~standard プロパティを持つオブジェクトを返すだけです。次は、環境変数のポート番号(文字列)を検証して数値に変換する例です。
import type { StandardSchemaV1 } from "@standard-schema/spec";
const port: StandardSchemaV1<string, number> = {
"~standard": {
version: 1,
vendor: "my-env",
validate(value) {
const n = typeof value === "string" && /^[0-9]+$/.test(value) ? Number(value) : NaN;
return Number.isInteger(n) && n > 0 && n < 65536
? { value: n }
: { issues: [{ message: "1〜65535 の整数文字列が必要です" }] };
},
},
};
port["~standard"].validate("3000"); // { value: 3000 }
port["~standard"].validate("abc"); // { issues: [ { message: "1〜65535 の整数文字列が必要です" } ] }
型引数の1つ目が入力、2つ目が出力の型です。types は省略できますが、省略しても InferOutput<typeof port> は number に推論されました。公式は、非同期が必要な場合を除き validate() から Promise を返さないよう勧めています。同期で返しておけば、前の章の同期専用APIにも渡せます。
@standard-schema/specの入れ方:dependenciesに置く理由
仕様パッケージのインストールは必須ではありません。公式FAQは、型定義をそのままコピーして自分のコードに貼る方法も認めており、メジャーバージョンを上げない限り破壊的変更をしないと明言しています。パッケージを使う場合は npm install @standard-schema/spec(Denoは jsr:@standard-schema/spec)で入れ、import type で読み込みます。実行時のコードは含まれません。
注意点は依存の種類です。型しか含まないので devDependencies に入れたくなりますが、公式FAQはこれを明確に否定しています。スキーマを受け取る関数を公開すると、StandardSchemaV1 がそのライブラリの公開APIの一部になり、利用者が本番用にインストールしたときにも型が解決できなければならないためです。アプリケーション内部で使うだけなら devDependencies でも動きますが、npmに公開するライブラリでは dependencies に置きます。
npmの週間ダウンロード数は1億5,354万回(2026年9月28日〜10月4日)です。他パッケージの依存として入った分を含む数で、直接インストールした数ではありません。
フレームワーク別の受け取り方の違い
「Standard Schemaに対応」と書かれていても、受け取ったスキーマをどう使うかはツールごとに違います。同じZod・Valibot・ArkTypeのスキーマを4つのツールに渡して挙動を確かめました。
tRPC 11.19.0:ライブラリ独自のparseの優先
@trpc/server 11.19.0の入力パーサーは、~standard を最後の手段として使います。parseAsync・parse・assert などライブラリ独自のメソッドがあればそちらを先に呼ぶ実装です。そのため、入力エラー時に TRPCError の cause に入る例外はライブラリで変わります。
| スキーマ | cause の型 | cause.issues |
|---|---|---|
| Zod 4.6.5 | ZodError | あり |
| Valibot 1.5.0 | StandardSchemaV1Error | あり |
| ArkType 2.2.8 | TraversalError | 無し(arkErrors) |
errorFormatter で error.cause.issues を読む処理を書いている場合、ArkTypeに切り替えるとエラー詳細が消えます。ライブラリを差し替えられる設計にしたいなら、cause の型ごとに分岐する処理を入れておきます。tRPCの全体像はtRPCとは?REST・GraphQL・gRPCとの違いと型安全なAPI実装をv11の実コードで解説を参照してください。
Hono:既定の400レスポンスに含まれる入力値
Honoでは @hono/standard-validator(0.4.0)の sValidator がStandard Schemaを受け取ります。検証に失敗したとき、第3引数のフックを渡さないと、レスポンス本文に受け取った入力値(data)と、ライブラリが返したエラーの配列(error)がそのまま入ります。hono 4.13.13で、パスワードが短すぎるログイン要求を送ると、400のレスポンスに送信したパスワードの文字列が含まれていました。
import { Hono } from "hono";
import { sValidator } from "@hono/standard-validator";
import * as v from "valibot";
const Login = v.object({ password: v.pipe(v.string(), v.minLength(12)) });
const app = new Hono();
app.post(
"/login",
sValidator("json", Login, (result, c) => {
if (!result.success) {
return c.json({ errors: ["入力内容を確認してください"] }, 400); // 入力値を返さない
}
}),
(c) => c.text("ok"),
);
フックで固定の文面を返すと、本文は {"errors":["入力内容を確認してください"]} になります。ライブラリのメッセージだけを返す方法では足りません。Valibot 1.5.0で password に数値の 123456 を送ると、メッセージ自体が「Invalid type: Expected string but received 123456」となり、入力値が含まれます。認証情報や個人情報を受け取るエンドポイントでは、フックで固定メッセージなどを返し、入力値を含み得るライブラリのメッセージも直接返さないようにします。
React Hook Form:standardSchemaResolverのエラー型は空文字
React Hook Formでは、@hookform/resolvers の standardSchemaResolver を使います。v4.0.0(2025年2月)で追加されたリゾルバーで、@hookform/resolvers/standard-schema から読み込みます。
import { useForm } from "react-hook-form";
import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
import * as z from "zod";
const schema = z.object({ email: z.email() });
export function useEmailForm() {
return useForm({
resolver: standardSchemaResolver(schema), // Valibot・ArkTypeのスキーマも渡せる
});
}
5.9.1でZod・Valibot・ArkTypeのスキーマを渡すと、どれも errors.email.message にライブラリの文面が入りました。一方で errors.email.type は空文字です。仕様のエラーに種類を表す項目が無いためで、zodResolver では入る "too_small" のようなコードを使って表示を出し分けている画面は、standardSchemaResolver に替えると分岐できなくなります。React Hook Form自体の使い方はReact Hook Formとは?使い方・バリデーション・v7の書き方を実例で解説で扱っています。
TanStack Form 1.33.5:同期バリデータにPromiseを返すスキーマは例外
TanStack Formは validators.onChange などにスキーマをそのまま渡せます。@tanstack/form-core 1.33.5の実装は、onChange・onBlur のような同期のバリデータで validate() が Promise を返すと「async function passed to sync validator」という例外を投げます。yupのスキーマは onChangeAsync などの非同期側に渡す必要があります。型エラーにはならないため、実行するまで気付けません。詳しくはTanStack Formとは?使い方とReact Hook Formとの違い・zod/yup検証の落とし穴【v1.33】を参照してください。
Standard Schemaで標準化されない範囲と採用の判断
Standard Schemaが揃えるのは「検証して、成功なら値、失敗ならメッセージとパスを返す」部分と型推論だけです。次の機能は対象外で、必要ならライブラリ固有のAPIを使うことになります。
- エラーのコード:
too_smallのような種類が無いため、コードをキーにした多言語化や表示の出し分けはできません。文面はライブラリが出すものをそのまま使うか、ライブラリ側で日本語化しておきます。 - デフォルト値とメタデータ:スキーマから初期値や説明文を取り出す方法は定められていません。
- スキーマの中身の参照:どのフィールドが必須か、といった構造は読めません。JSON Schemaに変換する用途はStandard JSON Schemaが別に受け持ちますが、対応ライブラリはまだ限られます。
採用の判断は、自分が「スキーマを受け取る側」かどうかで決まります。利用者にスキーマを渡してもらうライブラリ・フレームワーク・社内共通基盤を作るなら、StandardSchemaV1 を受け取る形にするのが現時点での定石です。Zod専用の引数にすると、ValibotやArkTypeを選んだチームが使えません。NestJSの設定モジュール @nestjs/config も12.0.0でこの形に移っています(@nestjs/config(ConfigModule)の使い方|v12のZod検証・envFilePath・型安全化)。
逆に、アプリケーションのコードでZodだけを使っているなら、自分で ~standard を経由する理由はありません。safeParse の方がエラーのコードや詳細を扱え、得られる情報が多いからです。Standard Schemaの恩恵は、使っているフォームやAPIのライブラリが対応していることで間接的に受けられます。
よくある質問
Standard Schemaとは何ですか?
Zod・Valibot・ArkTypeなどのスキーマ検証ライブラリが共通で実装する、TypeScriptのインターフェース仕様です。スキーマに ~standard プロパティを持たせることで、ツール側はライブラリごとのアダプター無しに検証と型推論ができます。仕様はZod・Valibot・ArkTypeの作者が共同で設計しました。
StandardSchemaV1とJSON Schemaは何が違いますか?
JSON SchemaはJSONデータの構造を記述するための言語仕様で、スキーマ自体がJSONで書かれます。StandardSchemaV1 はTypeScriptのオブジェクトが持つべきインターフェースで、検証関数と型情報を共通化するものです。両者をつなぐのが1.1.0で加わったStandard JSON Schemaで、対応スキーマからJSON Schemaを生成するメソッドを定めています。
standardSchemaResolverとzodResolverはどちらを使えばよいですか?
Zodだけを使うならzodResolverで十分で、エラーの type にZodのコードが入ります。standardSchemaResolverはZod・Valibot・ArkTypeなどを同じ書き方で扱えますが、type は空文字になります。スキーマのライブラリを混在させる、または将来入れ替える予定があるならstandardSchemaResolverを選びます。
@standard-schema/specのインストールは必要ですか?
スキーマを使うだけのアプリケーションでは不要です。Zodなどの対応ライブラリが ~standard を実装済みだからです。スキーマを受け取るライブラリを作る場合は、型定義をコピーするか、パッケージを dependencies に入れて import type で使います。
Zodはどのバージョンから対応していますか?
Zod 3.24.0以降でStandard Schemaに対応しています。Zod 4系はすべて対応しており、JSON Schemaへの変換を定めたStandard JSON Schemaには4.2以降が対応しています。3.23以前ではスキーマに ~standard が無いため、対応ツールに渡せません。