goでjsonを臨機応変に扱う
2020-07-10
azblob://2022/11/11/eyecatch/2020-07-10-treat-json-flexibly-with-go-000.jpg

名古屋事業所の松枝です。
ここ半年くらい仕事もブログもインフラばかりだったので、久しぶりにアプリの記事を書こうと思います。

最近はスクリプト言語を使う時に何を使うのが良いか迷います。
いや、もともと大して選んでいないんですが、Azure関連の操作がメインだったのでPowerShellをよく使っています。
PowerShellはC#っぽく書けるので悪くないんですが、改めて、OS関係なく軽快に動いてくれる言語としてgoに注目しています。
プログラミングのリハビリを兼ねてgoでGrafanaを扱うコードを書いていたんですが、jsonを扱うところで苦労したのでノウハウをメモしておこうと思います。
encoding/json を使用する場合のお話になります。

1. 礼儀正しく扱う

jsonの構造をすべて把握している場合は、その構造を表現したtypeを宣言し、json.Unmarshal で変換された値を突っ込むことができます。
一部定義するだけでも良いようです。

例えばGrafanaで GET /api/search/ を実行した結果のjsonを処理したい場合を考えます。https://grafana.com/docs/grafana/latest/http_api/folder_dashboard_search/

得られるjsonには、id、uid、title、・・・といくつかの値が得られますが、例えばその中でtitleとuidの値だけ拾いたいときは下記のような感じです。

package main

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

type DashboardOverview struct {
	Title string `json:"title"`
	Uid   string `json:"uid"`
}

func main() {
	dashboardJson := 【Grafanaからjsonを取得する処理】
	var dashboards []DashboardOverview
	if err := json.Unmarshal(dashboardJson, &dashboards); err != nil {
		log.Fatal(err)
	}
	fmt.Println(dashboards)
}

こうすると、変数 dashboards にダッシュボードのtitleとuidが配列で格納されます。

2. 子要素を文字列として扱う

jsonのある要素以下の内容をごっそり文字列で取り出したいときは、json.RawMessage という型を使えばよいみたいです。

例えばGrafanaで GET /api/dashboards/uid/:uid を実行した結果のjsonを処理したい場合を考えます。https://grafana.com/docs/grafana/latest/http_api/dashboard/#get-dashboard-by-uid

得られるデータの中に大きく dashboard と metadata があります。
dashboardの中のjsonだけ抽出したいときは下記のような感じです。

package main

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

type DashboardDetail struct {
	Meta      json.RawMessage `json:"meta"`
	Dashboard json.RawMessage `json:"dashboard"`
}

func main() {
	detailJson := 【Grafanaからjsonを取得する処理】
	var detail DashboardDetail
	if err := json.Unmarshal(detailJson, &detail); err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(detail.Dashboard))
}

こうすると、detail.Dashboard にdashboard要素の内容が入りますので、stringに変換するなどして扱います。

3. ある特定の値だけピンポイントに操作する

ある特定の値だけ扱いたいときは、interface{} という箱に入れてしまえばよいみたいです。
型アサーションを使用すれば参照、更新できました。

例えばエクスポートしたGrafanaのダッシュボードのjsonのインポートを自動化することを考えます。対応するAPIは POST /api/dashboards/db ですが、これはダッシュボード定義のidの値がnullならCreate、値が入っていたらUpdateとなります。
https://grafana.com/docs/grafana/latest/http_api/dashboard/#create-update-dashboard

エクスポートしたjsonにはidが入った状態なので、新規環境にダッシュボードを追加する場合は何らかの方法でidをnullにする必要があります。
idの定義は深い階層にもあるので、正規表現での置換は難しそうです。
この場合は json.Unmarshal して、一番浅いidの値を更新する処理を書くことになります。
下記のような感じです。

package main

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

func main() {
	detailJson := 【Grafanaからjsonを取得する処理】
	var jsonObj interface{}
	err := json.Unmarshal(detailJson, &jsonObj)
	if err != nil {
		log.Fatal(err)
	}
	jsonObj.(map[string]interface{})["id"] = nil

	jsonByteArray, _ := json.Marshal(jsonObj)

	fmt.Println(string(jsonByteArray))
}

これでidをnullにできたので、APIでダッシュボードを作成することができます。
値の更新が終わったら、json.Marshal をすればbyte配列にすることができますよ。

まとめ

これだけ押さえておけばjsonを自在に扱える気になってきました。
ちなみに標準ライブラリ以外のものを使えばもっと楽なようなので、制約がない場合はいろいろ調べてみると良いと思いました。