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-Type を application/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)
})writeJSON と writeError の 2 つを用意しておけば、ハンドラ内のレスポンス処理がほぼワンライナーになります。
w.Header().Set と json.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 では有効にしておくとよいでしょう。