Go言語における動的JSONの処理方法

統計情報を扱うJSON解析モジュールを設計する場合、以下のようなJSON形式を想定します。

{
    "category": "異なるJSONデータを識別するためのフィールド",
    "payload": "実際のネストされたデータ"
}

実装コード:

package main

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

type Container struct {
	Category string
	Payload  interface{} // 任意の型を受け入れる
}

type Audio struct {
	Detail      string
	Source      string
}

type Bell struct {
	Increase bool
}

func main() {
	audioData := Container{
		Category: "audio",
		Payload: Audio{
			Detail:      "explosive",
			Source:      "the Bruce Dickinson",
		},
	}
	buffer, err := json.Marshal(audioData)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%s\n", buffer)

	bellData := Container{
		Category: "bell",
		Payload: Bell{
			Increase: true,
		},
	}
	buffer, err = json.Marshal(bellData)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%s\n", buffer)
}

Payloadフィールドをinterface{}型として定義し、任意の型を受け入れるようにしています。次に、payload内のフィールドを解析してみましょう。

const jsonData = `
{
	"category": "audio",
	"payload": {
		"detail": "explosive",
		"source": "the Bruce Dickinson"
	}
}
`
var container Container
if err := json.Unmarshal([]byte(jsonData), &container); err != nil {
	log.Fatal(err)
}
// Gopherの名において、このような記述は避けてください
var detail string = container.Payload.(map[string]interface{})["detail"].(string)
fmt.Println(detail)

より良い方法として、*json.RawMessageを使用し、payloadフィールドの解析を後回しにできます。

type Container struct {
	Category string
	Payload  *json.RawMessage
}

interface{}と*json.RawMessageを組み合わせた完全な例:

package main

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

const jsonData = `
{
	"category": "audio",
	"payload": {
		"detail": "explosive",
		"source": "the Bruce Dickinson"
	}
}
`

type Container struct {
	Category string
	Payload  interface{}
}

type Audio struct {
	Detail      string
	Source      string
}

func main() {
	var rawData json.RawMessage
	container := Container{
		Payload: &rawData,
	}
	if err := json.Unmarshal([]byte(jsonData), &container); err != nil {
		log.Fatal(err)
	}
	switch container.Category {
	case "audio":
		var audioData Audio
		if err := json.Unmarshal(rawData, &audioData); err != nil {
			log.Fatal(err)
		}
		var detail string = audioData.Detail
		fmt.Println(detail)
	default:
		log.Fatalf("不明なメッセージカテゴリ: %q", container.Category)
	}
}

最初の部分は以上です。さらに改善できるポイントがいくつかあります。

  1. JSONデータ内のcategoryフィールドを列挙定数として抽出します。github.com/campoy/jsonenums を使用します。
//go:generate jsonenums -type=MessageType

type MessageType int

const (
	audio MessageType = iota
	bell
)

上記を定義した後、以下のコマンドを実行します。

jsonenums -type=MessageType

このモジュールは*_jsonenums.goファイルを自動生成し、以下のようなメソッドが定義されます。

func (t MessageType) MarshalJSON() ([]byte, error)
func (t *MessageType) UnmarshalJSON([]byte) error

これにより、カスタムしたMessageTypeとJSON型間のシリアライズ・デシリアライズ処理が自動的に実装されます。

  1. 異なるJSON categoryフィールドに対して、対応する構造体を返すメソッドを定義できます。
var typeResolvers = map[MessageType]func() interface{}{
	audio: func() interface{} { return &AudioContent{} },
	bell:  func() interface{} { return &BellContent{} },
}
  1. 1と2を組み合わせて、以前のswitchブロックを削除します。

完全なコード:

type Application struct {
	// アプリケーション状態を保持
}

// Operationはアプリケーションで実行可能な処理を表すインターフェース
type Operation interface {
	Execute(app *Application) error
}

type BellContent struct {
	// ...
}

func (c *BellContent) Execute(app *Application) error {
	// ...
}

type AudioContent struct {
	// ...
}

func (c *AudioContent) Execute(app *Application) error {
	// ...
}

var typeResolvers = map[MessageType]func() Operation{
	audio: func() Operation { return &AudioContent{} },
	bell:  func() Operation { return &BellContent{} },
}

func main() {
	app := &Application{
		// ...
	}

	// 受信メッセージを処理
	var raw json.RawMessage
	container := Container{
		Payload: &raw,
	}
	if err := json.Unmarshal([]byte(jsonData), &container); err != nil {
		log.Fatal(err)
	}
	content := typeResolvers[container.Category]()
	if err := json.Unmarshal(raw, content); err != nil {
		log.Fatal(err)
	}
	if err := content.Execute(app); err != nil {
		// ...
	}
}

別のアプローチとして、定義されたJSONフィールドがすべて最上位に配置されているケースを考えます。つまり、ネストされたpayloadフィールドがない場合です。

{
    "category": "異なるJSONデータを識別するためのフィールド",
    ...
}

この場合はJSONを2回デシリアライズする必要があります。最初にcategoryフィールドを確認し、異なるcategoryに応じてもう一度デシリアライズを行います。

package main

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

const jsonData = `
{
	"category": "audio",
	"detail": "explosive",
	"source": "the Bruce Dickinson"
}
`

type Container struct {
	Category string
}

type Audio struct {
	Detail      string
	Source      string
}

func main() {
	var container Container
	data := []byte(jsonData)
	if err := json.Unmarshal(data, &container); err != nil {
		log.Fatal(err)
	}
	switch container.Category {
	case "audio":
		var combined struct {
			Container
			Audio
		}
		if err := json.Unmarshal(data, &combined); err != nil {
			log.Fatal(err)
		}
		var detail string = combined.Detail
		fmt.Println(detail)
	default:
		log.Fatalf("不明なメッセージカテゴリ: %q", container.Category)
	}
}

タグ: golang JSON parsing dynamic-json Reflection

5月21日 05:41 投稿