Parameter validationの特徴比較
はじめに
HTTP リクエストには,どのようなパラメタが含まれているでしょうか?
以下のリクエストの例を見てみましょう.
POST /v1/users/123/profile?lang=ja HTTP/1.1
Host: example.com
Content-Type: application/json
X-Auth-Token: 456abcd
Cookie: session_id=789efgh
{
"name": "たろう",
"age": 30,
"email": "taro@example.com"
}
このリクエストには,以下のパラメタが含まれています:
- パスパラメタ:
123
- クエリパラメタ:
lang=ja
- ヘッダー:
X-Auth-Token: 456abcd
- Cookie:
session_id=sid-789efgh
- リクエストボディ[1]:
{"name": "たろう", "age": 30, "email": "taro@example.com"}
ご覧の通り,HTTP リクエストはテキストベースであり,パラメタもすべて文字列として表現されます.
そのため,サーバーサイドでパラメタを利用する際には,パラメタ検証(parameter validation) によって期待通りの形式であるかどうかを確認することが重要となります.
本記事では,パラメタ検証の特徴を比較するために,いくつかのプログラミング言語やフレームワークにおけるパラメタ検証機能を取り上げます.
上記の例について,それぞれどのようにパラメタを検証するのか見ていきましょう.
パラメタ検証ライブラリの比較
FastAPI/Pydantic
FastAPI は Python 製の Web フレームワークであり,Pydantic は FastAPI で使われるデータ検証ライブラリです.
コントローラを実装するとき,引数に型ヒントを記述することでパラメタを検証できます.
from typing import Literal
from fastapi import Body, Cookie, FastAPI, Header, Path, Query, Request
from pydantic import BaseModel, EmailStr, Field
class User(BaseModel):
# User name should be a string of 1 to 100 characters.
name: str = Field(min_length=1, max_length=100)
# User age should be a non-negative integer.
age: int = Field(ge=0)
# User email should be a valid email address.
# This works after installing `pydantic[email]` extra package.
email: EmailStr
app = FastAPI(root_path="/v1")
@app.post("/users/{user_id}/profile")
async def update_user_profile(
# Path parameter `user_id` should be an positive integer.
user_id: int = Path(gt=0),
# Query parameter `lang` should be "ja" or "en" if given.
lang: Literal["ja", "en"] | None = Query(None, regex=r"^(ja|en)$"),
# Header `X-Auth-Token` should be a string of 7 characters.
x_auth_token: str = Header(min_length=7, max_length=7),
# Cookie `session_id` should be match the pattern `sid-[a-z0-9]{7}`.
session_id: str = Cookie(regex=r"^sid-[a-z0-9]{7}$"),
# Request body should be a JSON object of `User` model.
user: User,
) -> dict:
# Implementation of the controller here...
検証に失敗した場合は,FastAPI が自動的に 422 Unprocessable Entity エラーを返します.
エラーハンドラを実装することでこの挙動を変更できます.
https://fastapi.tiangolo.com/tutorial/handling-errors/#override-request-validation-exceptions
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
return JSONResponse(status_code=400, content={"error": str(exc)})
Express/express-validator
Express は Node.js 製の Web フレームワークであり,express-validator は Express で使われるデータ検証ライブラリです.
コントローラを実装するとき,ミドルウェアとして express-validator を用いることでパラメタを検証できます.
const express = require('express')
const { body, cookie, header, param, query, validationResult } = require('express-validator')
const bodyParser = require('body-parser')
const cookieParser = require('cookie-parser')
const app = express()
app.use(bodyParser.json())
app.use(cookieParser())
app.post(
'/v1/users/:user_id/profile',
param('user_id').isInt({ min: 1 }),
query('lang').optional().isIn(['ja', 'en']),
body('name').isString().isLength({ min: 1, max: 100 }),
body('age').isInt({ min: 0 }),
body('email').isEmail(),
header('x-auth-token').isLength({ min: 7, max: 7 }),
cookie('session_id').matches(/^sid-[a-z0-9]{7}$/),
(req, res) => {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() })
}
// Implementation of the controller here...
}
)
検証に失敗した場合の挙動は,開発者が明示的に実装する必要があります.
上記のように validationResult
関数でエラーを取得し,それに応じたレスポンスを返します.
また,上記の例ではパスパラメタ user_id
に対して正の整数であることを検証していますが,値を取得する際には再度文字列を数値に変換する必要があることに注意が必要です.
import { matchedData } from 'express-validator'
app.post(..., (req, res) => {
const user_id = matchedData(req).user_id
// `user_id` is a string, not a number.
})
Hono/Zod
Hono も同じく Node.js 製の Web フレームワークです.
Zod は TypeScript の汎用的なスキーマ検証ライブラリであり,Hono では推奨ライブラリとして公式ドキュメントで紹介されています.
コントローラを実装するとき,ミドルウェアとして zValidator
を用いることで Zod を用いたパラメタ検証を容易に実装できます.
import { Hono } from 'hono'
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'
const app = new Hono()
enum SupportedLang {
ja = 'ja',
en = 'en',
}
const User = z.object({
name: z.string().min(1).max(100),
age: z.number().int().min(0),
email: z.string().email(),
})
app.post(
'/v1/users/:user_id/profile',
zValidator('param', z.object({ user_id: z.coerce.number().int().positive() })),
zValidator('query', z.object({ lang: z.nativeEnum(SupportedLang).optional() })),
zValidator('json', User),
zValidator('header', z.object({ 'x-auth-token': z.string().length(7) })),
zValidator('cookie', z.object({ session_id: z.string().regex(/^sid-[a-z0-9]{7}$/) })),
async (c) => {
const { user_id } = c.req.valid('param')
// Implementation of the controller here...
}
)
検証に失敗した場合は,Hono が自動的に 400 Bad Request エラーを返します.
zValidator()
関数の第 3 引数にフックとしてエラー処理を行うこともできます.
zValidator('json', User, (result, c) => {
if (!result.success) {
return c.json({ error: result.errors }, 400)
}
})
Phoenix/Params
Phoenix は Elixir 製の Web フレームワークです.
Params は Ecto.Schema
を利用したパラメタ検証ライブラリであり,Phoenix におけるパラメタ検証としても利用できます.
defparams
マクロと Ecto.Schema
によってパラメタのスキーマを定義し,Ecto.Changeset
を取得することでパラメタを検証できます.
パスパラメタ,クエリパラメタ,リクエストボディは区別されず,すべて同列に扱われます.
また,この方法ではヘッダーやクッキーの検証は行えません.
defmodule MyAppWeb.UserController do
use MyAppWeb, :controller
use Params
defparams change_profile_params %{
user_id!: :integer,
lang: :string,
name!: :string,
age!: :integer,
email!: :string
}
def change_profile(conn, params) do
changeset = change_profile_params(params)
if changeset.valid? do
validated_params = Params.data(changeset)
# Implementation of the controller here...
else
conn
|> put_status(400)
|> json(%{error: inspect(changeset.errors)})
end
end
end
Params は Phoenix とは独立の汎用的なライブラリであるため,エラーハンドリングなどの連携は弱く,基本的には開発者が明示的に実装する必要があります.
Antikythera/ParamsValidator
Antikythera は,当社で開発を進めている Web フレームワークです.
複数の Web アプリケーション(gear)を 1 つの Erlang ノードクラスタ内で管理し動作させることができる点が特徴です.
この度,Antikythera.Plug.ParamsValidator
というパラメタ検証機能が追加されました.
このプラグを導入することで,コントローラの実装を大きく変えることなくパラメタ検証の機能を追加できます.
パラメタの形式は,リクエストボディであれば Antikythera.BodyJsonStruct
,それ以外であれば Antikythera.ParamStringStruct
を用いて定義します.
各フィールドは,Croma ライブラリを用いて形式を指定できます.
use Croma
defmodule MyGear.Controller.User do
use Antikythera.Controller
defmodule PathMatches do
use Antikythera.ParamStringStruct, fields: [user_id: Croma.PosInteger]
end
defmodule QueryParams do
defmodule SupportedLang do
use Croma.SubtypeOfAtom, values: [:ja, :en]
def from_string(x) when x in ~w(ja en), do: {:ok, String.to_atom(x)}
def from_string(_), do: {:error, :invalid}
end
use Antikythera.ParamStringStruct, fields: [lang: {Croma.TypeGen.nilable(SupportedLang), &SupportedLang.from_string/1}]
end
defmodule BodyJson do
defmodule Name, do: use Croma.SubtypeOfString, pattern: ~r/\A.{1,100}\z/
defmodule Age, do: use Croma.SubtypeOfInt, min: 0
defmodule Email, do: use Croma.SubtypeOfString, pattern: ~r<\A[A-Za-z0-9.!#$%&’*+/=?^_`{|}~-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\z>
use Antikythera.BodyJsonStruct,
fields: [
name: Name,
age: {Age, &String.to_integer/1},
email: Email
]
end
defmodule Headers do
defmodule XAuthToken, do: use Croma.SubtypeOfString, pattern: ~r/\A[A-Za-z0-9]{7}\z/
use Antikythera.HeaderStruct, fields: ["x-auth-token": XAuthToken]
end
defmodule Cookies do
defmodule SessionId, do: use Croma.SubtypeOfString, pattern: ~r/\Asid-[a-z0-9]{7}\z/
use Antikythera.CookieStruct, fields: ["session_id": SessionId]
end
plug Antikythera.Plug.ParamsValidator, :validate, [
path_matches: PathMatches,
query_params: QueryParams,
body: BodyJson,
headers: Headers,
cookies: Cookies
]
defun update_user_profile(%Conn{assigns: %{validated: validated}}) :: v[Conn.t] do
%PathMatches{user_id: user_id} = validated.path_matches
# Implementation of the controller here...
end
end
検証に失敗した場合は,Antikythera が自動的に 400 Bad Request エラーを返します.
Gear アプリケーションごとにエラーハンドリングをカスタマイズできます.
defmodule MyGear.Controller.Error do
use Antikythera.Controller
def parameter_validation_error(conn, parameter_type, {reason_type, _mods}) do
Conn.json(conn, 400, %{
error: "Parameter validation failed",
parameter_type: parameter_type,
reason: reason_type
})
end
end
まとめ
ここまでパラメタ検証機能をいくつかのプログラミング言語・フレームワークで比較しました.
個人的には,FastAPI/Pydantic は型ヒントで手軽にパラメタ検証ができる点,Hono/Zod は汎用的なライブラリである Zod をミドルウェアによって自然に組み込める点が利用しやすく,非常に参考になると感じました.
パラメタ検証機能の選定の基準として,また新たな Web フレームワークにおけるパラメタ検証機能の開発においても,この比較が参考になれば幸いです.
参考
- FastAPI/Pydantic
- Express/express-validator
- Hono/Zod
- Phoenix/Params
- Antikythera/ParamsValidator
-
この例では JSON ですが,フォームデータやマルチパートデータなど他の形式の場合もあります. ↩︎
Discussion