Golang で JSON 形式の文字列やファイルを扱う (encoding/json)

Golang 標準パッケージの encoding/json を使用すると、Golang のオブジェクトと JSON テキストを相互に変換することができます。

import "encoding/json"

Go と JSON の型を変換するとき、次のように対応付けられます(Channel など、一部扱えないデータはあります)。

Go の型JSON の型
boolboolean
float64number
stringstring
[]interface{}配列
map[string]interface{}オブジェクト
nilnull

デフォルトでは、Go の構造体のフィールド名がそのまま JSON のプロパティ名になりますが、構造体フィールドのタグで JSON のプロパティ名を指定する ことができます。 構造体のフィールドがポインタ型である場合は、ポインタが指す実際の値を使って JSON データに変換してくれます。

JSON のプロパティ名を指定する

Golang の構造体は、各フィールドにタグという情報を追加 できるようになっています。 encoding/json パッケージは、構造体の json タグを読み取って、JSON データに変換したときのプロパティ名として使用するようになっています。

type Person struct {
	Name string `json:"name"`
	Age  int    `json:"age"`
}

上記ように json タグ設定した構造体を、encoding/json パッケージの各種メソッドで JSON に変換すると、次のような感じの JSON データになります。 json タグで指定した通り、プロパティ名がすべて小文字になっていることが分かります。

{"name":"まく","age":14}

さらに、json タグでは次のようなオプション指定もできるようになっています(omitempty- の部分)。

type Book struct {
	Title  string `json:"title"`           // ベーシックな使い方
	Price  int    `json:"price,omitempty"` // ゼロ値の場合はこのフィールドを出力しない
	Author string `json:"-"`               // このフィールドは出力しない
}

Go オブジェクト → JSON 文字列

json.Marshal 関数を使用すると、任意の Go オブジェクト(構造体)を JSON 形式の文字列データに変換できます。

func json.Marshal(v any) ([]byte, error)

構造体の 公開されたフィールド(大文字で始まるもの)のみ が JSON 出力されます。 変換後のデータは、具体的には UTF-8 エンコーディング形式のバイト配列 ([]byte) です。 次の例では、Person 構造体のデータを JSON 文字列に変換しています。

type Person struct {
	Name string `json:"name"`
	Age  int    `json:"age"`
}

func main() {
	p := Person{"まく", 14}
	bytes, err := json.Marshal(p)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(string(bytes)) //=> {"name":"まく","age":14}
}

改行やインデントを含む整形された JSON テキストが欲しい場合は、json.Marshal の代わりに json.MarshalIndent 関数を使用します。

bytes, err := json.MarshalIndent(p, "", "  ")

JSON 文字列 → Go オブジェクト

JSON 形式の文字列データ ([]byte) を Go のオブジェクトに変換するには、json.Unmarshal 関数を使用します。

func json.Unmarshal(data []byte, v any) error

次の例では、JSON 文字列を Person 構造体のデータに変換しています。

type Person struct {
	Name string `json:"name"`
	Age  int    `json:"age"`
}

func main() {
	jsonBytes := []byte(`{"name":"まく","age":14}`)
	var p Person
	if err := json.Unmarshal(jsonBytes, &p); err != nil {
		log.Fatal(err)
	}

	fmt.Printf("%+v\n", p) //=> {name:まく age:14}
	fmt.Println(p.Name)    //=> まく
	fmt.Println(p.Age)     //=> 14
}

Go オブジェクト → JSON ファイル

Go の構造体を JSON ファイルに出力するには、os.File オブジェクトを json.NewEncoder に渡して Encoder を作成し、(*json.Encoder).Encode メソッドでデータを書き込みます。

func json.NewEncoder(w io.Writer) *json.Encoder
func (*json.Encoder).Encode(v any) error

次の例では、Person 構造体の内容を person.json ファイルに出力しています。

main.go
package main

import (
	"encoding/json"
	"log"
	"os"
)

type Person struct {
	Name string `json:"name"`
	Age  int    `json:"age"`
}

func main() {
	// 書き込むデータ
	p := Person{"まく", 14}

	// JSON ファイルを新規作成(既に存在する場合は上書き)
	file, err := os.Create("person.json")
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()

	// JSON ファイルに書き込み
	encoder := json.NewEncoder(file)
	if err := encoder.Encode(p); err != nil {
		log.Fatal(err)
	}
}
出力結果 (person.json)
{"name":"まく","age":14}

改行やインデントを含む整形された JSON ファイルとして出力したい場合は、Encode メソッドで出力する前に、SetIndent メソッドでインデントの設定を行います。

encoder := json.NewEncoder(file)
encoder.SetIndent("", "  ")  // スペース 2 文字をインデントに使う
出力結果 (person.json)
{
  "name": "まく",
  "age": 14
}

JSON ファイル → Go オブジェクト

JSON ファイルを読み込んで、Go のオブジェクトに変換するには、os.File オブジェクトを json.NewDecoder に渡して Decoder を作成し、(*json.Decoder).Decode メソッドで Go の変数へデータを展開します。

func json.NewDecoder(r io.Reader) *json.Decoder
func (*json.Decoder).Decode(v any) error

JSON のスキーマが完全に分かっている場合

次の例では、person.json ファイルの内容を Person 構造体に変換しています。

person.json(入力ファイル)
{
  "name": "まく",
  "age": 14
}
main.go
package main

import (
	"encoding/json"
	"log"
	"os"
)

type Person struct {
	Name string `json:"name"`
	Age  int    `json:"age"`
}

func main() {
	// JSON ファイルを読み出し用にオープン
	file, err := os.Open("person.json")
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()

	// JSON ファイルの内容を Person 構造体データとして読み出す
	var p Person
	decoder := json.NewDecoder(file)
	if err := decoder.Decode(&p); err != nil {
		log.Fatal(err)
	}

	log.Printf("%+v\n", p) //=> {name:まく age:14}
}

JSON のスキーマが不明な場合

具体的な構造体の型を設定せずに、map[string]any 型の変数に JSON ファイルの内容を読み出すこともできます。 これは、キーの型が文字列で、値の型が不明 (any / interface{}) なマップです。 この場合、少なくとも JSON ルート階層のフィールドはキー名で参照できますが、その値は any 型になるので、実際に値を扱う段階で型アサーションにより具体的な型を指定する必要があります。 あるいは、リフレクションで各フィールドの情報を列挙することが可能です。

// JSON ファイルの内容を map 型の Go オブジェクトとして読み出す
var rawJson map[string]any
decoder := json.NewDecoder(file)
if err := decoder.Decode(&rawJson); err != nil {
	log.Fatal(err)
}

// ルート階層のフィールドを参照する(値は any 型なので型アサーションして使う)
name := rawJson["name"].(string)
age := rawJson["age"].(float64)
fmt.Println(name)
fmt.Println(age)

// リフレクションで内容を調べても OK
refVal := reflect.ValueOf(rawJson)
for it := refVal.MapRange(); it.Next(); {
	fmt.Printf("key = %s\n", it.Key())
}