Skip to Content

Go Json ネストが深く配列もある場合のパース方法

概要

Goでネストが深いJsonをどう処理したら良いのか困り、やってみてわかった知見を紹介。
構造体を使ってパースします。

結論

一度分かってしまえばなんてことはない。
配列の部分は一番最後に書いてます。

Jsonのサンプル

ネストとそこそこ深く配列もでてきて困ったAWSのイベントを例に進める。

EC2インスタンスを停止した際のイベント内容全文

全文は長いの説明の中では抜粋して載せています。
どうなってるんだ??と思う部分が出てきたら全文を確認してみてください。
※ アカウント名など一部他の文字に置換済み

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
{
    "eventVersion": "1.05",
    "userIdentity": {
        "type": "AssumedRole",
        "principalId": "xxx:bigmuramura",
        "arn": "arn:aws:sts::000:assumed-role/bigmuramuraSwitchRole/bigmuramura",
        "accountId": "000",
        "accessKeyId": "xxx",
        "sessionContext": {
            "sessionIssuer": {
                "type": "Role",
                "principalId": "xxx",
                "arn": "arn:aws:iam::000:role/bigmuramuraSwitchRole",
                "accountId": "000",
                "userName": "bigmuramuraSwitchRole"
            },
            "webIdFederationData": {},
            "attributes": {
                "mfaAuthenticated": "true",
                "creationDate": "2020-01-13T07:21:04Z"
            }
        }
    },
    "eventTime": "2020-01-13T08:20:21Z",
    "eventSource": "ec2.amazonaws.com",
    "eventName": "StopInstances",
    "awsRegion": "ap-northeast-1",
    "sourceIPAddress": "14.9.145.100",
    "userAgent": "console.ec2.amazonaws.com",
    "requestParameters": {
        "instancesSet": {
            "items": [
                {
                    "instanceId": "i-01b39875c1d8031f9"
                }
            ]
        },
        "force": false
    },
    "responseElements": {
        "requestId": "be1b005f-383f-4096-9491-709bf7825619",
        "instancesSet": {
            "items": [
                {
                    "instanceId": "i-01b39875c1d8031f9",
                    "currentState": {
                        "code": 64,
                        "name": "stopping"
                    },
                    "previousState": {
                        "code": 16,
                        "name": "running"
                    }
                }
            ]
        }
    },
    "requestID": "be1b005f-383f-4096-9441-703bf7892619",
    "eventID": "63497817-361f-417b-9a65-5d7b514a00cf",
    "eventType": "AwsApiCall",
    "recipientAccountId": "000"
}

パース処理

準備

main.goと同ディレクトリにsample.jsonとして、先程のJsonを保存。
データ量も少ないため手軽なio.util.ReadFileを使ってJsonファイルを読み込みます。

ファイル読み込みだけ

filesample.jsonを読み込みます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"os"
)

func main() {
	file, err := ioutil.ReadFile("./sample.json")
	if err != nil {
		log.Println("ReadError: ", err)
		os.Exit(1)
	}

	fmt.Println(string(file)) //[]byteで受け取るためstringに変換する必要がある
}

出力結果

{
    "eventVersion": "1.05",
    "userIdentity": {
        "type": "AssumedRole",
        "principalId": "xxx:bigmuramura",
        省略

自動で構造体を生成

JSON-to-Go の左側(JSON)部分にJsonを貼り付けて構造体を生成してみます。
神サイトにより一発で構造体が出来上がりゴールかと思ったのですが、
構造体の中に構造体を定義していくと配列が出てきた時に扱いづらかったです。
そのため、構造体を複数に分割して定義した方が使い易いようなので、
実際そうしてみたら確かに使いやすくなりました。
この出力結果を利用しつつ、自前で構造体を定義していきます。

JSON-to-Goの出力結果

1
2
3
4
5
6
7
type AutoGenerated struct {
	EventVersion string `json:"eventVersion"`
	UserIdentity struct {
		Type           string `json:"type"`
		PrincipalID    string `json:"principalId"`
    Arn            string `json:"arn"`
    省略

1階層目の構造体を定義

ネストされていない1階層目の定義は簡単
eventVersion1.05を取り出したい場合

1
2
3
{
    "eventVersion": "1.05",
    省略

このように1階層目にある名前の構造体を定義する
JSON-to-Goの出力結果から必要な行をコピペすると手間が省ける

構造体の定義
1
2
3
type AwsEvents struct {
    EventVersion string `json:"eventVersion"` // この行はJSON-to-Goのコピペ
}
補足

構造体の名前AwsEventsは任意
フィールド名EventVersionの先頭は大文字しないといけない
フィールド名はJsonタグ(json:"eventVersion"の部分)と同じ名前である必要はない
フィールド名とJsonタグの名前が一致しない場合は、Jsonタグを明示しないといけない

この例ではフィルード名とJsonタグを一致した上で、Jsonタグを明示している
必要な部分はJSON-to-Goのコピペで済ませているから、Jsonタグも勝手につけてくれている

AwsEvetns型としてevを作成し、ev.EventVersionで中身を取り出せる

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
type AwsEvents struct {
	EventVersion string `json:"eventVersion"`
}

func main() {
	file, err := ioutil.ReadFile("./sample.json")
	if err != nil {
		log.Println("ReadError: ", err)
		os.Exit(1)
	}

	var ev AwsEvents
	json.Unmarshal(file, &ev)

  fmt.Println(ev.EventVersion)
}

出力結果

1.05

2階層目の構造体を定義

userIdentityの中にあるtypeAssumedRoleを取り出したい場合である
userIdentityという塊でネストされて1階層深くなったパターン

1
2
3
4
5
{
    "eventVersion": "1.05",
    "userIdentity": {
        "type": "AssumedRole",
        省略

構造体の定義

先程のAwsEventsの中にUserIdentityフィールドを追加し、UserIdentitys型とした
UserIdentitys構造体を別途作成し、その中にTypeフィールドを作成する

1
2
3
4
5
6
7
8
type AwsEvents struct {
	EventVersion string `json:"eventVersion"`
	UserIdentity UserIdentitys  // 2階層目を追加
}

type UserIdentitys struct { // 2階層目の構造体定義
	Type string `json:"type"` // この行はJSON-to-Goのコピペ
}

補足

UserIdentity(フィルード名) UserIdentitys(型)の箇所は、は任意の名前でOK

1
2
3
4
5
6
7
8
type AwsEvents struct {
	EventVersion string `json:"eventVersion"`
	UserIdentity nanka // 型名前をちゃんと合わせる
}

type Nanka struct { // 構造体の名前は任意
	Type string `json:"type"`
}

ev.UserIdentity.Typeで取り出せるようになる
.区切りで下の階層に潜っていける

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func main() {
	file, err := ioutil.ReadFile("./sample.json")
	if err != nil {
		log.Println("ReadError: ", err)
		os.Exit(1)
	}

	var ev AwsEvents
	json.Unmarshal(file, &ev)

	fmt.Println(ev.UserIdentity.Type)
}

出力結果

AssumedRole

3, 4階層目の構造体を定義

sessionIssuerの中のuserNameを取り出します

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{
    "eventVersion": "1.05",
    "userIdentity": {
        "type": "AssumedRole",
        "principalId": "xxx:bigmuramura",
        "arn": "arn:aws:sts::000:assumed-role/bigmuramuraSwitchRole/bigmuramura",
        "accountId": "000",
        "accessKeyId": "xxx",
        "sessionContext": {
            "sessionIssuer": {
                "type": "Role",
                "principalId": "xxx",
                "arn": "arn:aws:iam::000:role/bigmuramuraSwitchRole",
                "accountId": "000",
                "userName": "bigmuramuraSwitchRole"
            },

2階層目同様に新しい構造体の定義と、その上の階層のフィールドに名前を追加する

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
type AwsEvents struct {
	EventVersion string `json:"eventVersion"`
	UserIdentity UserIdentitys
}

type UserIdentitys struct {
	Type           string `json:"type"`
	SessionContext SessionContexts // 3階層目を追加
}

type SessionContexts struct { // 3階層名の構造体定義
	SessionIssuer SessionIssuers // 4階層目を追加
}

type SessionIssuers struct { // 4階層めの構造体定義
	UserName string `json:"userName"` // 欲しい値のところはJSON-to-Goのコピペ
}

ev.UserIdentity.SessionContext.SessionIssuer.UserNameで取り出せる

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func main() {
	file, err := ioutil.ReadFile("./sample.json")
	if err != nil {
		log.Println("ReadError: ", err)
		os.Exit(1)
	}

	var ev AwsEvents
	json.Unmarshal(file, &ev)

	fmt.Println(ev.UserIdentity.SessionContext.SessionIssuer.UserName)
}

出力結果

bigmuramuraSwitchRole

配列が出てきた場合の構造体定義

3階層目のitemsが配列形式になっている
配列の中のi-01b39875c1d8031f9と、stoppingを取得したい場合どうしたらいいかでハマった

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
省略
    "responseElements": {
        "requestId": "be1b005f-383f-4096-9491-709bf7825619",
        "instancesSet": {
            "items": [
                {
                    "instanceId": "i-01b39875c1d8031f9",
                    "currentState": {
                        "code": 64,
                        "name": "stopping"
                    },
                    省略

配列の構造体の定義ポイント

Itemsの配列のところは[]Itemsと書く
[]Itemsの構造体の定義はtype Items struct {}と書いて他と同じ書き方

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
type AwsEvents struct {
	ResponseElements ResponseElements `json:"responseElements"`
}

type ResponseElements struct {
	InstancesSet InstancesSet `json:"instancesSet"` // 2階層目
}

type InstancesSet struct {
	Items []Items `json:"items"` // 3階層目 配列はこんな感じで
}

type Items struct { // 配列だかと言って定義の仕方は同じ
	InstanceID   string       `json:"instanceId"`
	CurrentState CurrentState // 4階層目を追加
}

type CurrentState struct {
	Name string `json:"name"`
}

配列の中へのアクセスの仕方

Items[0]と書いてアクセスするのが、今までと配列へのアクセスの違い

1
	fmt.Println(ev.ResponseElements.InstancesSet.Items[0].InstanceID)
コード全文
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"os"
)

type AwsEvents struct {
	ResponseElements ResponseElements `json:"responseElements"`
}

type ResponseElements struct {
	InstancesSet InstancesSet `json:"instancesSet"` // 2階層目
}

type InstancesSet struct {
	Items []Items `json:"items"` // 3階層目 配列はこんな感じで
}

type Items struct { // 配列だかと言って定義の仕方は同じ
	InstanceID   string       `json:"instanceId"`
	CurrentState CurrentState // 4階層目を追加
}

type CurrentState struct {
	Name string `json:"name"`
}

func main() {
	file, err := ioutil.ReadFile("./sample.json")
	if err != nil {
		log.Println("ReadError: ", err)
		os.Exit(1)
	}

	var ev AwsEvents
	json.Unmarshal(file, &ev)

	fmt.Println(ev.ResponseElements.InstancesSet.Items[0].InstanceID)
	fmt.Println(ev.ResponseElements.InstancesSet.Items[0].CurrentState.Name)
}
実行結果
i-01b39875c1d8031f9
stopping

感想

AWSのJsonパースするの大変そうで逃げてたけど向き合えばなんてことはなかっった。
CloudWatch Events -> Lambdaで通知を作ってた時のJsonパースメモでした。

参考

https://qiita.com/tchnkmr/items/b686adc4a7e144d48755
https://qiita.com/NaoyukiSato/items/6dcd26725d01d0b6b2fa