英語614435 views
高校日本史190647 views
雑学1473736 views
ヒストリア291495 views
りんご212052 views
数学講師2891552 views
LaTeX962830 views
中学英語812106 views
高校倫理1441073 views
中学理科1631340 views
Help
Tools

English

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""(空文字列)
int0
boolfalse
ポインタ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.DecoderDisallowUnknownFields を使うと、構造体に定義されていないフィールドが 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 より、全エラーを一括で返すほうがユーザー体験は良くなるでしょう。

エラーを 1 つずつ返す

実装がシンプル。修正→送信→別のエラーのループが起きやすい。

全エラーを一括で返す

クライアントが一度にすべてを修正できる。フォームバリデーションに最適。

Go の標準ライブラリにはバリデーション専用の機構がないため、自前で書くか go-playground/validator のようなライブラリを使うことになります。フィールド数が少なければ手動バリデーションで十分ですが、10 個を超えるあたりからライブラリの導入を検討してください。