統計情報を扱う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)
}
}
最初の部分は以上です。さらに改善できるポイントがいくつかあります。
- 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型間のシリアライズ・デシリアライズ処理が自動的に実装されます。
- 異なるJSON categoryフィールドに対して、対応する構造体を返すメソッドを定義できます。
var typeResolvers = map[MessageType]func() interface{}{
audio: func() interface{} { return &AudioContent{} },
bell: func() interface{} { return &BellContent{} },
}
- 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)
}
}