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
}