この記事は Go Advent Calendar 2017 5日目の記事です。
はじめに
みなさんはじめまして、@cia_ranaと申します。
今回は json.Unmarshal
する際に時刻のフォーマットを自由に変更できるようにするためのライブラリtson
(ツォン)を開発したお話です。
tson
は †黒魔術† を使用しています。用法用量を守り正しくお使いください。
この記事で使う構造体や変数
ここでは本記事で使用する構造体や変数を定義します。hoge
とかhuga
とかでは頭に入ってきづらいと思うので、少し意味を持たせた内容となっています。
構造体Person
は、Person
の特徴Feature
と、Person
が作成された日付CreatedAt
を持ちます。
type Person struct {
Feature Feature `json:"feature"`
CreatedAt *time.Time `json:"created_at"`
}
構造体Feature
は、Person
が持つ特徴の内容を表しています。
type Feature struct {
Name string `json:"name"`
Birth *time.Time `json:"birth"`
}
変数jsonString
は、構造体Person
のオブジェクトにアンマーシャル1する対象となるJSON文字列です。特に時刻のフィールド(birth
と created_at
)が今回のキモとなるので覚えておいてください!
var jsonString = `
{
"feature": {
"name": "cia_rana",
"birth": "1987-10-08"
},
"created_at": "2017-12-05"
}
`
まずはライブラリを使ってみよう
tson
を使ったJSON文字列から構造体のオブジェクトへのアンマーシャルは、標準ライブラリencoding/json
の関数json.Unmarshal
と同じやり方、つまり関数tson.Unmarshal
でできます。
ただ通常のjson.Unmarshal
と違うのは、tson.Unmarshal
の手前で関数tson.SetLayout
を呼んでいるところです。tson.SetLayout
は時刻のフォーマットを変更する関数で、この関数を呼んだ後にtson.Unmarshal
する際は変更したフォーマットが使用されます。
var person Person
tson.SetLayout(`2006-01-02`)
err := tson.Unmarshal([]byte(jsonString), &person)
if err != nil {
fmt.Println("error:", err)
} else {
fmt.Println(person)
}
// => {{cia_rana 1987-10-08 00:00:00 +0000 UTC} 2017-12-05 00:00:00 +0000 UTC}
きちんとアンマーシャルできていますね!
一体この処理の裏側では何が行われているのでしょう?
この記事を書こうと思ったきっかけ
ところで・・・
GoでJSON文字列を構造体のオブジェクトにアンマーシャルしたとき、こんな経験した人いますよね?
var person Person
err := json.Unmarshal([]byte(jsonString), &person)
if err != nil {
fmt.Println("error:", err)
} else {
fmt.Println(person)
}
// => error: parsing time ""1988-10-10"" as ""2006-01-02T15:04:05Z07:00"": cannot parse """ as "T"
エラー!!?
そう、GoでJSON中の時刻を*time.Time
にマッピングするとき、その時刻はGoがもともと用意しているフォーマットで記述されていなければきちんとマッピングできません。
もうちょっと融通が効いても良いと思うのですが、こればかりはGoの仕組み上どうしようもないのです。
ちなみに時刻の文字列をtime.Time
型のオブジェクトに変換する場合、時刻のフォーマットを明示的に指定できるのですが、Goはその指定の仕方が特殊で、YYYY-MM-DD
のように指定するのではなく、プログラマが読みやすいフォーマットで指定できるようになっています。それゆえ細かい数字を覚えなくてはいけません・・・。
解決策
さて、エラーが出ないようにきちんとアンマーシャルするためには、普通は構造体内の*time.Time
型をUnmarshalJSON
を備えたオリジナルのTime型に変更しますよね?
ただ、いちいち構造体を書き換えてUnmarshalJSON
を実装するのは大変だし、ライブラリ内の構造体を利用したい場合その構造体を容易に変更できない場合すらあります。
そこで、プログラム処理中に元からある構造体を基に「自由に時刻のフォーマットを変更できる新しい構造体を動的に生成する」という新しいアプローチを提案します。
このアプローチによって、"まずはライブラリを使ってみよう"の章で見たように、用意された関数に構造体のオブジェクトを渡すだけで、あらかじめ設定した時刻のフォーマットに基づいてJSON文字列内の時刻を*time.Time
にうまいことマッピングしながらアンマーシャリングを行うことができるようになります。
以降はこのアプローチをどのように実現したのか、その中身を紐解いていきます。
さぁ、黒魔術をはじめよう
時刻のフォーマットを考慮しながらJSON文字列をPerson
オブジェクトにアンマーシャルする手順は次の通りです。
-
Person
の型情報を取得する -
Person
型情報内の*time.Time
型をtson
側で用意したオリジナルのTime
型に置換した新たな型を生成する - 新たな型のオブジェクトを生成する
- JSON文字列を新たな型のオブジェクトにアンマーシャルする
- 新たな型のオブジェクトを再びJSON文字列に変換する
- 再変換したJSON文字列を
Person
のオブジェクトにアンマーシャルする
reflect
を多用した黒魔術の香りがぷんぷんしますね
それでは、黒魔術の手順をそれぞれ見ていきましょう。
1. Person
の型情報を取得する
まずはベースとなるPerson
の型情報を取得します。
型情報を取得するには通常は関数reflect.TypeOf
を使います。ですが、Person
はポインタ型で入ってくる前提なので(tson.Unmarshal([]byte(jsonString), &person)
の&person
の部分より)、一旦ポインタが指す値を取得した後その型情報を取得します。ポインタが指す値を取得するには関数reflect.Elem
を使います。あとはその戻り値の関数Type
を呼んでやれば良いです。
var v {}interface := &person // `Person`はポインタで入ってくる
rv := reflect.ValueOf(v) // personの型情報を取得する。personの型情報は`reflect.Ptr`である
rt := rv.Elem().Type() // `reflect.Ptr`が指す値の型情報を取得する
これでPerson
の型情報が取得できました。
2. Person
型情報内の*time.Time
型をtson
側で用意したオリジナルのTime
型に置換した新たな型を生成する
ここでは、先ほど取得したPerson
の型情報に含まれる*time.Time
をオリジナルのTime
型(のポインタ)に置換していきます。ですが、その前にオリジナルのTime
型について説明します。
オリジナルのTime
型(tson.Time
と呼ぶ)は、関数tson.SetLayout
で指定した時刻のフォーマットに従ってJSON文字列内の時刻をtson.Time
にマッピングするという特徴を持っています。tson.SetLayout
で指定した時刻のフォーマットはtson
側のプライベートな変数format
に格納されています。また、時刻のフォーマットはスレッドセーフに指定できるようにしています。それ以外はよく見るtime.Time
のラッパーの形ですね。
var format = struct {
layout string
mutex *sync.Mutex
}{
time.RFC3339,
new(sync.Mutex),
}
func SetLayout(layout string) {
format.mutex.Lock()
format.layout = layout
format.mutex.Unlock()
}
type Time struct {
time.Time
}
func (t *Time) UnmarshalJSON(data []byte) error {
if string(data) == "null" {
return nil
}
tm, err := time.Parse(`"`+format.layout+`"`, string(data))
t.Time = tm
return err
}
tson.Time
の型情報は頻繁に使うため、tson
側のプライベートな変数rtt
であらかじめ持っておきます。
var rtt = reflect.TypeOf(&Time{})
さて、これで準備が整いました。
黒魔術をはじめましょう。
・・・と、カッコつけましたがやることはいたって単純で、*time.Time
を置換するだけです(笑)
ここでは便宜的にPerson
の型情報をrt
としておきます。
まずは、新しく生成する構造体のフィールドをまとめるスライスを定義します。スライスの型は[]reflect.StructField
です。フィールドの数はrt
のフィールドの数と同じであり、スライスの長さが分かっているので、メモリアロケーションの回数を減らす意味も込めてスライスの定義は次のように行います。
rs := make([]reflect.StructField, rt.NumField())
次にrt
のフィールドにアクセスします。型情報の種別がStruct
であるrt
のフィールドへのアクセスは、インデックスを用いて次のようにアクセスします。
f := rt.Field(i)
そして、ここから構造体のフィールドを置き換える作業になります。
戻り値f
のフィールドType
が*time.Time
である場合、それをtson.Time
の型情報で置き換えます。置き換えはただtson.Time
の型情報を代入するだけで良いです。
f.Type = rtt
一方、f.Type
が*time.Time
でない場合、一見何も置き換える必要がないと思いますよね?ですが、f.Type
の種別がArray
やSlice
、Struct
などの場合、それらの構成要素の中で*time.Time
が使われているかもしれません。
例えばf
がスライス[]*time.Time
の場合f.Type
はSlice
ですが、このスライスの要素の型は*time.Time
であり、置き換えの対象になります。また、f
の型の構成要素が直接*time.Time
を持たない場合でも、構成要素の構成要素が*time.Time
である可能性があります。つまり、再帰的にf
の構成要素にアクセスして置換していきます。
構成要素へのアクセスおよび置換された構成要素を持つ新しい型の生成は、それぞれの型の種別に合わせて行わなければいけません。
最後に、新しく生成する構造体のフィールドをまとめておいたスライスを、関数reflect.StructOf(rs)
によって新しく生成する構造体の型情報へ変換します。
以上をまとめると、次の関数newStruct
のようになります。
func newStruct(rt reflect.Type) reflect.Type {
rs := make([]reflect.StructField, rt.NumField())
for i := 0; i < rt.NumField(); i++ {
f := rt.Field(i)
if f.Type.String() == "*time.Time" {
f.Type = rtt
} else {
switch f.Type.Kind() {
case reflect.Array:
f.Type = reflect.ArrayOf(f.Type.Len(), newStruct(f.Type.Elem()))
case reflect.Chan:
f.Type = reflect.ChanOf(f.Type.ChanDir(), newStruct(f.Type.Elem()))
case reflect.Func:
ins := make([]reflect.Type, f.Type.NumIn())
outs := make([]reflect.Type, f.Type.NumOut())
for i := 0; i < f.Type.NumIn(); i++ {
ins[i] = newStruct(f.Type.In(i))
}
for i := 0; i < f.Type.NumOut(); i++ {
outs[i] = newStruct(f.Type.Out(i))
}
f.Type = reflect.FuncOf(ins, outs, f.Type.IsVariadic())
case reflect.Interface:
// TODO
case reflect.Map:
f.Type = reflect.MapOf(newStruct(f.Type.Key()), newStruct(f.Type.Elem()))
case reflect.Ptr:
f.Type = reflect.PtrTo(newStruct(f.Type.Elem()))
case reflect.Slice:
f.Type = reflect.SliceOf(newStruct(f.Type.Elem()))
case reflect.Struct:
f.Type = newStruct(f.Type)
case reflect.UnsafePointer:
// TODO
}
}
rs[i] = f
}
return reflect.StructOf(rs)
}
//TODO
としているところはまだ未実装のところですが、今回のデモでは必要ないので省略しました。
この記事の山場は乗り越えたので、ここからはサクサクと説明していきたいと思います!
3. 新たな型のオブジェクトを生成する
まずは先ほど新しく生成した構造体の型情報を変数rt
に格納しておきます。このrt
は2.のrt
とは別物です。
rt, err := NewStruct(v)
型情報rt
からオブジェクトを生成するには次のようにすれば良いです。新たに生成されたvi
の型はinterface{}
です。
vi := reflect.New(rt).Interface()
4. JSON文字列を新たな型のオブジェクトにアンマーシャルする
生成したオブジェクトにJSON文字列をいつもやってる通りアンマーシャルします。自分で指定した時刻のフォーマットに基づいてアンマーシャルするため、そのフォーマットの指定の仕方が間違っていなければきちんとアンマーシャルされるはずです。
err := json.Unmarshal(jsonBytes, &vi)
5. 新たな型のオブジェクトを再びJSON文字列に変換する
え、なんでここでJSON文字列を生成するの?と思われるかもしれません。これは最終的にPerson
のオブジェクトにJSON文字列をきちんとアンマーシャルするために行っています。元のJSON文字列中の時刻のフィールドがGo標準の時刻フォーマットに変換された新たなJSON文字列が生成されるので、何も弄っていないPerson
オブジェクトにJSON文字列をきちんとアンマーシャルできるようになるのです。
data, err := json.Marshal(vi)
6. 再変換したJSON文字列をPerson
のオブジェクトにアンマーシャルする
生成したJSON文字列をPerson
文字列にアンマーシャルします。5.で述べた通りきちんとアンマーシャルできるはずです。
err := json.Unmarshal(data, v)
おわりに
ここまで、黒魔術の内容についてざっくりと見てきました。文章量はそこそこありましたが、やってきたことはただ__型を置き換えた新たな構造体を動的に作る__ということだけでした。ただ、reflect
を多用しており速度面やコンパイル時の型チェックが不十分な場合があるので、冒頭でも述べましたが用法用量を守り正しくお使いください。
というネタでした。
参考文献
-
「パース」、「デシリアライズ」ともいう ↩