JSONリクエストの読み込み
Go標準ライブラリのnet/httpパッケージを利用して、JSON形式のリクエストを受け取るREST APIの作成方法を説明します。 エラー処理としてリクエストヘッダのチェックやデコードエラーのハンドリング方法についても説明します。
基本
json
パッケージを利用することで、JSONデータの内容を構造体に格納できます。
次はHTTPリクエストのボディにあるJSONデータを読み込むハンドラ関数です。
type Task struct {
ID string `json:"id"`
Title string `json:"title"`
Done bool `json:"done"`
Deleted bool `json:"-"`
}
func postTask(w http.ResponseWriter, r *http.Request) {
// JSONデータを読み込み、その結果を構造体Taskに格納する
var t Task
if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// 読み込んだ後の処理は割愛
}
エラー処理の強化
先ほどのコードでも動作しますが、次の通りエラー処理を追加します。
- HTTPリクエストヘッダの
Content-Type
をチェックする - JSONデコードエラーの原因がクライアントかサーバかを切り分け、適切なHTTPステータスコードを設定する
- JSONデコードエラーの原因分類をメッセージに含める
func postTask(w http.ResponseWriter, r *http.Request) {
// Content-Typeのチェック
ct := r.Header.Get("Content-Type")
if !strings.HasPrefix(ct, "application/json") {
e := "JSONで送ってください"
http.Error(w, e, http.StatusUnsupportedMediaType)
return
}
var t Task
if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
// クライアントが原因のエラーはHTTPステータスコード400を設定
// サーバが原因のエラーはHTTPステータスコード500を設定
// エラーメッセージはerr.Error()だけだと分かりづらいため、
// 原因分類を追加
var syntaxError *json.SyntaxError
var unmarshalTypeError *json.UnmarshalTypeError
switch {
case errors.As(err, &syntaxError):
e := fmt.Sprintf("invalid json syntax: %s", err.Error())
http.Error(w, e, http.StatusBadRequest)
case errors.As(err, &unmarshalTypeError):
e := fmt.Sprintf("invalid json field: %s", err.Error())
http.Error(w, e, http.StatusBadRequest)
case errors.Is(err, io.EOF):
e := fmt.Sprintf("request body is empty: %s", err.Error())
http.Error(w, e, http.StatusBadRequest)
case errors.Is(err, io.ErrUnexpectedEOF):
e := fmt.Sprintf("invalid json syntax: %s", err.Error())
http.Error(w, e, http.StatusBadRequest)
default:
http.Error(w, "", http.StatusInternalServerError)
// エラー内容のログ出力は割愛
}
return
}
w.WriteHeader(http.StatusOK)
}
ヘルパー関数
JSONリクエストの読み込みは各ハンドラで実行するため、ヘルパー関数を作成します。
加えて、レスポンスのデータ形式をJSONにします。
writeJSONError
関数はJSONレスポンスの書き込みで説明します。
func postTask(w http.ResponseWriter, r *http.Request) {
var t Task
if err := readJSON(w, r, &t); err != nil {
return
}
w.WriteHeader(http.StatusOK)
}
func readJSON(w http.ResponseWriter, r *http.Request, v interface{}) error {
ct := r.Header.Get("Content-Type")
if !strings.HasPrefix(ct, "application/json") {
msg := "Content-Type must be application/json: got %s"
err := fmt.Errorf(msg, ct)
writeJSONError(w, http.StatusUnsupportedMediaType, err.Error())
return err
}
if err := json.NewDecoder(r.Body).Decode(v); err != nil {
var syntaxError *json.SyntaxError
var unmarshalTypeError *json.UnmarshalTypeError
switch {
case errors.As(err, &syntaxError):
err = fmt.Errorf("invalid json syntax: %w", err)
writeJSONError(w, http.StatusBadRequest, err.Error())
case errors.As(err, &unmarshalTypeError):
err = fmt.Errorf("invalid json field: %w", err)
writeJSONError(w, http.StatusBadRequest, err.Error())
case errors.Is(err, io.EOF):
err = fmt.Errorf("request body is empty: %w", err)
writeJSONError(w, http.StatusBadRequest, err.Error())
case errors.Is(err, io.ErrUnexpectedEOF):
err = fmt.Errorf("invalid json syntax: %w", err)
writeJSONError(w, http.StatusBadRequest, err.Error())
default:
err = fmt.Errorf("failed to decode json: %w", err)
writeJSONError(w, http.StatusInternalServerError, "")
// エラー内容のログ出力は割愛
}
return err
}
return nil
}