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

Go

はじめに 🤔

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生成について学びましょう!

コメント

タイトルとURLをコピーしました