Go で JSON のバリデーションを行う:必須フィールドとゼロ値の落とし穴
Go の encoding/json は JSON を構造体にパースしてくれますが、値の妥当性までは検証しません。必須フィールドが欠けていてもエラーにならず、ゼロ値が静かにセットされるだけです。この挙動を理解しないと、不正なデータがバリデーションをすり抜けます。
ゼロ値の落とし穴
JSON にフィールドが含まれていない場合、Go はそのフィールドをゼロ値で初期化します。エラーにはなりません。
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}
func main() {
data := `{"email":"alice@example.com","age":25}`
var req CreateUserRequest
err := json.Unmarshal([]byte(data), &req)
fmt.Println(err) // <nil>(エラーなし)
fmt.Println(req.Name) // ""(空文字列=ゼロ値)
}Name が空文字列なのは「送られなかった」のか「意図的に空を送った」のか区別がつきません。int 型の Age も同様で、送られなかったのか本当に 0 なのかわかりません。これが「ゼロ値の落とし穴」です。
| string | ""(空文字列) |
| int | 0 |
| bool | false |
| ポインタ | nil |
| スライス | nil |
すべての型でゼロ値と未送信が同じ値になるため、encoding/json だけでは必須チェックができません。
手動バリデーションの基本
もっとも直接的な対策は、パース後に自分でチェックする方法です。
func validateCreateUser(req CreateUserRequest) error {
if req.Name == "" {
return fmt.Errorf("name は必須です")
}
if req.Email == "" {
return fmt.Errorf("email は必須です")
}
if !strings.Contains(req.Email, "@") {
return fmt.Errorf("email の形式が不正です")
}
if req.Age < 0 || req.Age > 150 {
return fmt.Errorf("age は 0〜150 の範囲で指定してください")
}
return nil
}func handler(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, "JSON が不正です", http.StatusBadRequest)
return
}
if err := validateCreateUser(req); err != nil {
writeError(w, err.Error(), http.StatusBadRequest)
return
}
// バリデーション通過後の処理
}JSON をデコード
バリデーション関数でチェック
エラーがあれば 400 を返す
通過したら本処理へ
シンプルで分かりやすい方法ですが、フィールドが増えるとバリデーション関数が肥大化します。また「未送信の空文字列」と「意図的な空文字列」は依然として区別できません。
ポインタで未送信を検出する
フィールドをポインタ型にすると、JSON に含まれていない場合は nil がセットされます。
type UpdateUserRequest struct {
Name *string `json:"name"`
Email *string `json:"email"`
Age *int `json:"age"`
}
func main() {
data := `{"email":"new@example.com"}`
var req UpdateUserRequest
json.Unmarshal([]byte(data), &req)
fmt.Println(req.Name) // <nil>(送られなかった)
fmt.Println(req.Email) // &"new@example.com"(値あり)
fmt.Println(req.Age) // <nil>(送られなかった)
}フィールドなし → ゼロ値。未送信と空値を区別できない。コードはシンプル。
フィールドなし → nil。未送信を正確に検出できる。ただし nil チェックが必要でコードが冗長になりやすい。
PATCH リクエストのような部分更新では、この区別が特に重要です。nil なら変更しない、非 nil なら送られた値で上書きする、という制御が可能になります。
ただし JSON の null と「フィールドなし」はどちらも nil になる点には注意してください。これを区別したい場合はさらに工夫が必要です。
json.RawMessage で存在を確認する
フィールドの「存在」と「値」を完全に分離する方法として、map[string]json.RawMessage で一旦パースするやり方があります。
func validateRequired(data []byte, fields ...string) error {
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
for _, field := range fields {
val, exists := raw[field]
if !exists {
return fmt.Errorf("%s は必須です", field)
}
if string(val) == "null" {
return fmt.Errorf("%s に null は指定できません", field)
}
}
return nil
}func handler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
if err := validateRequired(body, "name", "email"); err != nil {
writeError(w, err.Error(), http.StatusBadRequest)
return
}
var req CreateUserRequest
json.Unmarshal(body, &req)
// 通常の処理へ
}この方法なら「キーがない」「キーはあるが null」「キーも値もある」の 3 パターンを正確に区別できます。リクエストボディを 2 回パースするコストはありますが、正確さが求められる場面では有効な手段です。
DisallowUnknownFields で余分なフィールドを弾く
json.Decoder の DisallowUnknownFields を使うと、構造体に定義されていないフィールドが JSON に含まれていた場合にエラーを返します。
func handler(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
var req CreateUserRequest
if err := decoder.Decode(&req); err != nil {
// "json: unknown field \"namae\"" のようなエラー
writeError(w, err.Error(), http.StatusBadRequest)
return
}
}クライアントがフィールド名をタイポした場合に即座にエラーを返せるため、デバッグ時間を短縮できます。「name を送ったはずなのに反映されない」というバグの原因が、実は namae と書いていた、という類のミスを防げます。
エラーレスポンスを構造化する
バリデーションエラーは JSON で統一的に返すのが実務のセオリーです。
type ValidationError struct {
Field string `json:"field"`
Message string `json:"message"`
}
type ErrorResponse struct {
Errors []ValidationError `json:"errors"`
}
func validateCreateUser(req CreateUserRequest) []ValidationError {
var errs []ValidationError
if req.Name == "" {
errs = append(errs, ValidationError{
Field: "name", Message: "name は必須です",
})
}
if req.Email == "" {
errs = append(errs, ValidationError{
Field: "email", Message: "email は必須です",
})
}
if req.Email != "" && !strings.Contains(req.Email, "@") {
errs = append(errs, ValidationError{
Field: "email", Message: "email の形式が不正です",
})
}
return errs
}// レスポンス例
{
"errors": [
{"field": "name", "message": "name は必須です"},
{"field": "email", "message": "email は必須です"}
]
}フィールドごとにエラーを返せば、クライアントはどの入力に問題があるか一目でわかります。エラーを一つずつ返して何度もリトライさせる API より、全エラーを一括で返すほうがユーザー体験は良くなるでしょう。
実装がシンプル。修正→送信→別のエラーのループが起きやすい。
クライアントが一度にすべてを修正できる。フォームバリデーションに最適。
Go の標準ライブラリにはバリデーション専用の機構がないため、自前で書くか go-playground/validator のようなライブラリを使うことになります。フィールド数が少なければ手動バリデーションで十分ですが、10 個を超えるあたりからライブラリの導入を検討してください。











