[Goのはじめ方] Part25: JSONの扱い(encoding/json)

はじめに

JSON (JavaScript Object Notation) は、Web APIや設定ファイルなど、様々な場面でデータを交換するために広く使われている軽量なデータ形式です。 Go言語には、このJSONデータを簡単に扱うための強力な標準パッケージ encoding/json が用意されています。

このパッケージを使えば、Goのデータ構造(構造体やマップなど)とJSON文字列を相互に変換することができます。 このブログ記事では、encoding/json パッケージの基本的な使い方を学んでいきましょう!

Goのデータ構造からJSONへのエンコード (Marshal)

Goのデータ構造(例えば、構造体やマップ)をJSON形式のバイト列([]byte)に変換することを「マーシャリング (Marshalling)」または「エンコード (Encoding)」と呼びます。 これには json.Marshal 関数を使用します。

構造体をJSONに変換する例を見てみましょう。

package main
import (	"encoding/json"	"fmt"	"log"
)
type User struct {	ID int	Name string	Age int
}
func main() {	user := User{ID: 1, Name: "Gopher", Age: 10}	// 構造体をJSONバイト列にマーシャリング	jsonData, err := json.Marshal(user)	if err != nil {	log.Fatalf("JSONマーシャリングエラー: %s", err)	}	// バイト列を文字列として出力	fmt.Println(string(jsonData)) // 出力: {"ID":1,"Name":"Gopher","Age":10}
}

json.Marshal は、Goのデータ構造を受け取り、JSON形式のバイト列とエラーを返します。 Goの構造体のフィールド名は、デフォルトではそのままJSONのキー名になります。 ただし、JSONとしてエンコードされるのはエクスポートされたフィールド(大文字で始まるフィールド)のみです。

フィールドタグでJSON出力をカスタマイズ

JSONのキー名をGoのフィールド名と違う名前にしたい場合や、特定の条件下でフィールドを省略したい場合があります。 このような場合、構造体のフィールドに「タグ (Tag)」を付与することで、エンコード時の挙動をカスタマイズできます。

タグはバッククォート(`)で囲み、json:"..." の形式で指定します。

package main
import (	"encoding/json"	"fmt"	"log"
)
type Product struct {	ProductID int `json:"product_id"` // JSONキー名を "product_id" にする	Name string `json:"name"` // JSONキー名を "name" にする	Description string `json:"description,omitempty"` // 値が空ならJSONに含めない	Stock int `json:"-"` // このフィールドはJSONに含めない
}
func main() {	product1 := Product{ProductID: 101, Name: "Go T-Shirt", Description: "Official Go Gopher T-shirt", Stock: 50}	jsonData1, _ := json.Marshal(product1)	fmt.Println(string(jsonData1)) // 出力: {"product_id":101,"name":"Go T-Shirt","description":"Official Go Gopher T-shirt"}	product2 := Product{ProductID: 102, Name: "Go Sticker", Stock: 100} // Descriptionは空	jsonData2, _ := json.Marshal(product2)	fmt.Println(string(jsonData2)) // 出力: {"product_id":102,"name":"Go Sticker"} (descriptionはomitemptyにより省略)
}

よく使われるタグオプション:

  • json:"キー名": JSONでのキー名を指定します。
  • json:",omitempty": フィールドの値がGoのゼロ値(数値なら0, 文字列なら””, ポインタならnilなど)の場合、そのフィールドをJSON出力に含めません。
  • json:"-": このフィールドをJSONのエンコード・デコード対象から完全に除外します。
  • json:"キー名,omitempty": キー名を指定し、かつ値がゼロ値なら省略します。

マップやスライスも同様に json.Marshal でJSONに変換できます。

package main
import (	"encoding/json"	"fmt"
)
func main() {	// スライスのマーシャリング	langs := []string{"Go", "Python", "JavaScript"}	langsJson, _ := json.Marshal(langs)	fmt.Println(string(langsJson)) // 出力: ["Go","Python","JavaScript"]	// マップのマーシャリング	counts := map[string]int{"apple": 5, "banana": 10}	countsJson, _ := json.Marshal(counts)	fmt.Println(string(countsJson)) // 出力: {"apple":5,"banana":10}
}

JSONからGoのデータ構造へのデコード (Unmarshal)

JSON形式のバイト列をGoのデータ構造(構造体やマップなど)に変換することを「アンマーシャリング (Unmarshalling)」または「デコード (Decoding)」と呼びます。 これには json.Unmarshal 関数を使用します。

package main
import (	"encoding/json"	"fmt"	"log"
)
type Point struct {	X int `json:"x"`	Y int `json:"y"`
}
func main() {	jsonInput := []byte(`{"x":10, "y":20}`)	var p Point	// JSONバイト列をPoint構造体にアンマーシャリング	// 第2引数には、変換結果を格納する変数へのポインタを渡す	err := json.Unmarshal(jsonInput, &p)	if err != nil {	log.Fatalf("JSONアンマーシャリングエラー: %s", err)	}	fmt.Printf("Point: X=%d, Y=%d\n", p.X, p.Y) // 出力: Point: X=10, Y=20
}

json.Unmarshal は、JSONバイト列と、変換結果を格納する変数へのポインタを受け取ります。 JSONのキーとGoの構造体フィールドのマッピングは、Marshal と同様にタグを考慮して行われます。 大文字・小文字の違いは基本的に無視されますが、明確にするためにタグを使うことが推奨されます。

JSONの構造が事前にわからない場合や、柔軟に扱いたい場合は、map[string]interface{} を使うこともできます。 interface{} (またはエイリアスの any) は、任意の型の値を保持できます。

package main
import (	"encoding/json"	"fmt"	"log"
)
func main() {	jsonInput := []byte(`{"name": "Gopher", "active": true, "details": {"age": 10, "projects": ["Go", "Web"]}}`)	var data map[string]interface{} // any でも可	err := json.Unmarshal(jsonInput, &data)	if err != nil {	log.Fatalf("JSONアンマーシャリングエラー: %s", err)	}	// 値にアクセスするには型アサーションが必要	name := data["name"].(string)	active := data["active"].(bool)	details := data["details"].(map[string]interface{})	age := details["age"].(float64) // JSONの数値はデフォルトで float64 になることに注意	fmt.Printf("Name: %s, Active: %t, Age: %f\n", name, active, age)	// 出力: Name: Gopher, Active: true, Age: 10.000000
}

interface{} を使うと柔軟ですが、値を利用する際に型アサーションが必要になり、型安全性が低下する点に注意が必要です。 JSONの数値は、Goではデフォルトで float64 としてデコードされます。

ストリーム処理 (Encoder/Decoder)

ファイルやネットワーク接続など、データをストリームとして扱う場合は、json.Encoderjson.Decoder を使うのが便利です。 これらは io.Writerio.Reader インターフェースと連携します。

Encoder は、GoのデータをJSON形式で io.Writer(ファイル、HTTPレスポンスなど)に書き込みます。

package main
import (	"encoding/json"	"os"
)
type Config struct {	Server string `json:"server"`	Port int `json:"port"`
}
func main() {	cfg := Config{Server: "localhost", Port: 8080}	// 標準出力 (os.Stdout) に書き込むEncoderを作成	encoder := json.NewEncoder(os.Stdout)	encoder.SetIndent("", " ") // 人が読みやすいようにインデントを設定	// 構造体をJSONとしてエンコードして書き込む	err := encoder.Encode(cfg)	if err != nil {	log.Println("エンコードエラー:", err)	}	// 出力:	// {	// "server": "localhost",	// "port": 8080	// }
}

Decoder は、io.Reader(ファイル、HTTPリクエストボディなど)からJSONデータを読み取り、Goのデータ構造にデコードします。 大きなJSONデータや、連続するJSONオブジェクトを効率的に処理するのに役立ちます。

package main
import (	"encoding/json"	"fmt"	"log"	"strings"
)
type Message struct {	User string `json:"user"`	Text string `json:"text"`
}
func main() {	// 連続するJSONオブジェクトを含む文字列	jsonStream := `	{"user": "Alice", "text": "Hello!"}	{"user": "Bob", "text": "Hi Alice!"}	`	reader := strings.NewReader(jsonStream)	// readerから読み取るDecoderを作成	decoder := json.NewDecoder(reader)	// ストリームが終わるまでJSONオブジェクトをデコード	for decoder.More() {	var msg Message	err := decoder.Decode(&msg)	if err != nil {	log.Fatal("デコードエラー:", err)	}	fmt.Printf("User: %s, Text: %s\n", msg.User, msg.Text)	}	// 出力:	// User: Alice, Text: Hello!	// User: Bob, Text: Hi Alice!
}

Marshal/Unmarshal はデータ全体をメモリに読み込むため、非常に大きなJSONデータを扱う場合は Encoder/Decoder を使う方がメモリ効率が良い場合があります。

よくあるパターンと注意点

  • キー名とフィールド名のマッピング: JSONでよく使われる snake_casecamelCase のキー名を、Goの PascalCase (or CamelCase) のフィールド名に対応させるには、構造体タグ json:"..." を使いましょう。
  • 数値型: JSONの数値は、Goではデフォルトで float64 にデコードされます。整数として扱いたい場合は、デコード先の構造体のフィールドを int などにしておくか、json.Number 型を使って後で変換します。
    package main
    import (	"encoding/json"	"fmt"	"log"
    )
    func main() {	jsonInput := []byte(`{"value": 123}`)	var data map[string]json.Number // json.Numberを使う	err := json.Unmarshal(jsonInput, &data)	if err != nil { log.Fatal(err) }	// json.Numberからint64に変換	intValue, err := data["value"].Int64()	if err != nil { log.Fatal(err) }	fmt.Println("Int value:", intValue) // 出力: Int value: 123	// json.Numberからfloat64に変換	floatValue, err := data["value"].Float64()	if err != nil { log.Fatal(err) }	fmt.Println("Float value:", floatValue) // 出力: Float value: 123
    }
  • Null値: JSONの null は、Goのポインタ型 (*string, *int など) や interface{}, map, slicenil に対応します。値型(string, int など)のフィールドに null をデコードしようとするとエラーになることがあります。ポインタ型を使うか、omitempty タグを検討しましょう。
  • エラーハンドリング:MarshalUnmarshal, Encode, Decode はエラーを返す可能性があります。不正なJSONデータや型変換のエラーなど、必ずエラーチェックを行いましょう。
  • 未知のキー: デフォルトでは、Unmarshal は構造体に定義されていないJSONキーを無視します。未知のキーが存在する場合にエラーとしたい場合は、DecoderDisallowUnknownFields() メソッドを使用します。
    package main
    import (	"encoding/json"	"fmt"	"log"	"strings"
    )
    type Simple struct {	Key string `json:"key"`
    }
    func main() {	jsonInput := `{"key": "value", "unknown_key": "ignored?"}`	reader := strings.NewReader(jsonInput)	decoder := json.NewDecoder(reader)	decoder.DisallowUnknownFields() // 未知のフィールドをエラーにする設定	var s Simple	err := decoder.Decode(&s)	if err != nil {	// ここでエラーが発生する	log.Printf("デコードエラー (未知のフィールドあり): %v", err)	} else {	fmt.Printf("Decoded: %+v\n", s)	}
    }

まとめ

Goの encoding/json パッケージを使うことで、JSONデータのエンコード(マーシャリング)とデコード(アンマーシャリング)を簡単に行うことができます。

  • json.Marshal: Goのデータ構造をJSONバイト列に変換
  • json.Unmarshal: JSONバイト列をGoのデータ構造に変換
  • 構造体タグ: JSONキー名のカスタマイズ、フィールドの省略、除外などに使用
  • json.Encoder / json.Decoder: ファイルやネットワークなどのストリーム処理に便利

Web APIとの連携や設定ファイルの読み書きなど、JSONは多くの場面で活用されます。 このパッケージの使い方をマスターして、Goでの開発をさらに効率的に進めましょう!

次は、テンプレート処理(html/template)に進んで、動的なHTML生成について学びましょう!

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です