Go で JSON API を作る:リクエストの受け取りからレスポンスの返却まで

Go で JSON API を構築するとき、リクエストの受け取りからレスポンスの返却まで、すべて標準ライブラリの encoding/json パッケージで対応できます。外部ライブラリなしで JSON をやりとりする基本パターンを押さえておきましょう。

JSON レスポンスを返す

まずはもっとも基本的な「構造体を JSON として返す」パターンです。

package main

import (
	"encoding/json"
	"net/http"
)

type Message struct {
	Text   string `json:"text"`
	Status int    `json:"status"`
}

func main() {
	http.HandleFunc("/api/hello", func(w http.ResponseWriter, r *http.Request) {
		msg := Message{Text: "こんにちは", Status: 200}

		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(msg)
	})

	http.ListenAndServe(":8080", nil)
}

json.NewEncoder(w).Encode は構造体を JSON に変換し、そのまま ResponseWriter に書き出します。Content-Typeapplication/json に設定するのは必須です。省略するとブラウザやクライアントが JSON として解釈してくれない場合があります。

構造体のフィールドタグ json:"text" はキー名を制御しています。タグを省略すると Go のフィールド名(大文字始まり)がそのまま JSON のキーになるため、Text のように不自然なキー名になってしまいます。

JSON リクエストを受け取る

クライアントから送られてきた JSON を構造体にパースするには、json.NewDecoder を使います。

type CreateRequest struct {
	Name  string `json:"name"`
	Email string `json:"email"`
}

http.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
		return
	}

	var req CreateRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "JSON の形式が不正です", http.StatusBadRequest)
		return
	}

	// req.Name, req.Email を使って処理
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusCreated)
	json.NewEncoder(w).Encode(req)
})

クライアントが JSON を POST

json.NewDecoder で構造体にパース

バリデーション・ビジネスロジック

json.NewEncoder で JSON レスポンスを返却

Decode はリクエストボディを直接読み取るため、別途 io.ReadAll する必要がありません。また、構造体に存在しないフィールドが JSON に含まれていても、デフォルトでは無視されます。

バリデーションを入れる

JSON のパースに成功しても、値が妥当かどうかは別の問題です。空文字やゼロ値のチェックは自分で書く必要があります。

func validateCreateRequest(req CreateRequest) error {
	if req.Name == "" {
		return fmt.Errorf("name は必須です")
	}
	if req.Email == "" {
		return fmt.Errorf("email は必須です")
	}
	if !strings.Contains(req.Email, "@") {
		return fmt.Errorf("email の形式が不正です")
	}
	return nil
}

バリデーションエラーをクライアントに返すときは、エラーメッセージも JSON で返すと統一感が出ます。

type ErrorResponse struct {
	Error string `json:"error"`
}

func writeError(w http.ResponseWriter, msg string, code int) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(code)
	json.NewEncoder(w).Encode(ErrorResponse{Error: msg})
}

こうしておけば、クライアントは常に JSON をパースすればよくなります。レスポンスが HTML だったり JSON だったりすると、クライアント側の処理が煩雑になるためです。

レスポンスヘルパーを作る

JSON レスポンスを返すたびに Content-Type の設定と Encode を書くのは冗長です。ヘルパー関数にまとめておくとコードがすっきりします。

func writeJSON(w http.ResponseWriter, code int, data interface{}) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(code)
	json.NewEncoder(w).Encode(data)
}

使い方はシンプルです。

http.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {
	users := []User{
		{ID: 1, Name: "Alice"},
		{ID: 2, Name: "Bob"},
	}
	writeJSON(w, http.StatusOK, users)
})

writeJSONwriteError の 2 つを用意しておけば、ハンドラ内のレスポンス処理がほぼワンライナーになります。

毎回手書き

w.Header().Setjson.NewEncoder を毎回書く。コードの重複が増え、Content-Type の設定忘れが起きやすい。

ヘルパー関数

writeJSON(w, 200, data) の一行で済む。レスポンス形式の変更も一箇所で対応可能。

未知のフィールドを検出する

デフォルトでは、リクエスト JSON に構造体にないフィールドが含まれていてもエラーになりません。厳密な API を作りたい場合は DisallowUnknownFields を使います。

decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()

var req CreateRequest
if err := decoder.Decode(&req); err != nil {
	writeError(w, "不明なフィールドが含まれています", http.StatusBadRequest)
	return
}

この設定を入れると、{"name":"Alice","age":30} のように CreateRequest に存在しない age フィールドが来た場合にエラーを返せます。タイポの検出にも役立つので、厳密さが求められる API では有効にしておくとよいでしょう。