はじめに 🤔
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.Encoder
と json.Decoder
を使うのが便利です。
これらは io.Writer
や io.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_case
やcamelCase
のキー名を、GoのPascalCase
(orCamelCase
) のフィールド名に対応させるには、構造体タグ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
,slice
のnil
に対応します。値型(string
,int
など)のフィールドにnull
をデコードしようとするとエラーになることがあります。ポインタ型を使うか、omitempty
タグを検討しましょう。 -
エラーハンドリング:
Marshal
やUnmarshal
,Encode
,Decode
はエラーを返す可能性があります。不正なJSONデータや型変換のエラーなど、必ずエラーチェックを行いましょう。 ✅ -
未知のキー:
デフォルトでは、
Unmarshal
は構造体に定義されていないJSONキーを無視します。未知のキーが存在する場合にエラーとしたい場合は、Decoder
のDisallowUnknownFields()
メソッドを使用します。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生成について学びましょう!
コメント