# go言語でjsonやXMLに含まれる特定フォーマットの文字列フィールドをパースして専用の型に入れたい 例1 json/XMLに"192.168.0.1"を値として持つフィールドがあり、それをgo言語のnet.IP型としてパースしたい。 例2 json/XMLに"PT1440M"を値として持つフィールドがあり、をISO8601継続時間型としてgo言語のtime.Duration型としてパースしたい。 ■ ISO8601とは > ISO 8601は、日付と時刻の表記に関するISOの国際規格である。 > (略) > ISO 8601形式の時刻表記例 > 基本形式 20220622T124204+0900 > 拡張形式 2022-06-22T12:42:04+09:00 > [wikipedia](https://ja.wikipedia.org/wiki/ISO_8601)より ■ ISO8601継続時間とは > 継続時間は、ある期間中に含まれる時間の合計を定義し、P[n]Y[n]M[n]DT[n]H[n]M[n]S、または、右に示すように、P[n]Wの形式で表される。 > (略) > P は期間を表す指定子(period を表す)であり、継続時間表現の先頭に置かれる。 > Y は年の指定子であり、年を表す数値の後に置かれる。 > M は月の指定子であり、月を表す数値の後に置かれる。 > W は週の指定子であり、週を表す数値の後に置かれる。 > D は日の指定子であり、日を表す数値の後に置かれる。 > T 時間の指定子であり、継続時間表現の時間の部分の前に置く。 > H は時間の指定子であり、時間を表す数値の後に置かれる。 > M は分の指定子であり、分を表す数値の後に置かれる。 > S は秒の指定子であり、秒を表す数値の後に置かれる。 > たとえば、P3Y6M4DT12H30M5Sは、「3年、6か月、4日、12時間、30分、5秒」という継続時間を表現している。 > [wikipedia](https://ja.wikipedia.org/wiki/ISO_8601#%E7%B6%99%E7%B6%9A%E6%99%82%E9%96%93)より ## 文字列フィールドを文字列としてパースする場合 まず json/XMLに含まれる文字列フィールドを文字列としてパースする方法をおさらいしておく。 一般的に以下のように書く。 (エラーハンドリングは省略。以降同じ。) ```go package main import ( "encoding/json" "encoding/xml" "fmt" ) type Hoge struct { IPAddress string `json:"ipaddress" xml:"IPAddress"` Duration string `json:"duration" xml:"Duration"` } func (me Hoge) String() string { return fmt.Sprintf("%s %s", me.IPAddress, me.Duration) } func main() { jb := []byte(` { "ipaddress":"192.168.0.1", "duration":"PT1440M" }`) var objFromJson Hoge json.Unmarshal(jb, &objFromJson) fmt.Printf("json:%s\n", objFromJson.String()) xb := []byte(` <?xml version="1.0"?> <Hoge> <IPAddress>192.168.0.1</IPAddress> <Duration>PT1440M</Duration> </Hoge> `) var objFromXml Hoge xml.Unmarshal(xb, &objFromXml) fmt.Printf("xml:%s\n", objFromJson.String()) } ``` 実行結果 ``` json:192.168.0.1 PT1440M xml:192.168.0.1 PT1440M ``` 以下の部分を、stringではなくnet.IP型、time.Duration型として読むように修正したい。 ```go= ... type Hoge struct { IPAddress string `json:"ipaddress" xml:"IPAddress"` Duration string `json:"duration" xml:"Duration"` } ... ``` ## 方法1. UnmarshalJSON()/UnmarshalXML()を使う IPアドレス文字列は以下の方法でnet.IP型にパースできる。 ```go import ("net") ... ip := net.ParseIP("192.168.0.1") ``` ISO8601継続時間文字列はそのままではtime.Duration型にパースできない。 ISO8601継続時間型を扱うライブラリ(ここでは"github.com/rickb777/date/period")を使えば以下の方法でtime.Duration型にパースできる。 ```go import (iso8601 "github.com/rickb777/date/period") ... iso8601duration, _ := iso8601.Parse("PT1440M") duration, _ = iso8601duration.Duration() ``` 上記をjson/XMLを構造体に読み込む際に呼び出されるメソッドUnmarshalJSON()/UnmarshalXML()に組み込む。 ```go package main import ( "encoding/json" "encoding/xml" "fmt" "net" "time" iso8601 "github.com/rickb777/date/period" ) type Hoge struct { IPAddress net.IP `json:"ipaddress" xml:"IPAddress"` Duration time.Duration `json:"duration" xml:"Duration"` } func (me Hoge) String() string { return fmt.Sprintf("%s %s", me.IPAddress.String(), me.Duration.String()) } func (me *Hoge) UnmarshalJSON(b []byte) error { dummy := struct { IPAddress string `json:"ipaddress"` Duration string `json:"duration"` }{} json.Unmarshal(b, &dummy) me.IPAddress = net.ParseIP(dummy.IPAddress) iso8601duration, _ := iso8601.Parse(dummy.Duration) me.Duration, _ = iso8601duration.Duration() return nil } func (me *Hoge) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { dummy := struct { IPAddress string `xml:"IPAddress"` Duration string `xml:"Duration"` }{} d.DecodeElement(&dummy, &start); me.IPAddress = net.ParseIP(dummy.IPAddress) iso8601duration, _ := iso8601.Parse(dummy.Duration) me.Duration, _ = iso8601duration.Duration() return nil } func main() { jb := []byte(` { "ipaddress":"192.168.0.1", "duration":"PT1440M" }`) var objFromJson Hoge json.Unmarshal(jb, &objFromJson) fmt.Printf("json:%s\n", objFromJson.String()) xb := []byte(` <?xml version="1.0"?> <Hoge> <IPAddress>192.168.0.1</IPAddress> <Duration>PT1440M</Duration> </Hoge> `) var objFromXml Hoge xml.Unmarshal(xb, &objFromXml) fmt.Printf("xml:%s\n", objFromJson.String()) } ``` UnmarshalJSON()/UnmarshalXML()内部で、一旦、各フィールドの値を文字列として吸い出してしまうのがミソ。 UnmarshalJSON()/UnmarshalXML()は ```go ... json.Unmarshal(jb, &objFromJson) ... xml.Unmarshal(xb, &objFromXml) ``` で呼び出される。 実行結果 ``` json:192.168.0.1 24h0m0s xml:192.168.0.1 24h0m0s ``` この方法の場合はjsonとXMLでそれぞれ - UnmarshalJSON() - UnmarshalXML() を用意する必要がある。 ## 方法2. UnmarshalText()を使えそうなら使う json/XMLを読み込む構造体のフィールドの型がUnmarshalText()を持っている場合はそれを使うことができる。UnmarshalText()には文字列をパースする処理が実装済みである。 フィールドの型がUnmarshalText()を持っていない場合は、自分で用意した型ならUnmarshalText()を実装できる。([goDoc](https://pkg.go.dev/encoding#TextUnmarshaler)) 今回のjson/XML構造体内のフィールドのnet.IP型はUnmarshalText()を持つ。([goDoc](https://pkg.go.dev/net#IP.UnmarshalText)) time.Durationはそれ自体ではISO8601継続時間文字列をパースできないので、time.Duration型に変わって、"github.com/rickb777/date/period"のperiod型をフィールドとして持たせる。 このperiod型はUnmarshalText()を持つ。([goDoc](https://pkg.go.dev/github.com/rickb777/date/period#Period.UnmarshalText)) よって、方法1の場合とは違って - json/XMLそれぞれにUnmarshalJSON()/UnmarshalXML()のようなメソッドを用意する必要がない。 - 各フィールドのためのパースメソッド(例:net.ParseIP())を呼び出す必要がない。 ```go package main import ( "encoding/json" "encoding/xml" "fmt" "net" iso8601 "github.com/rickb777/date/period" ) type Hoge struct { IPAddress net.IP `json:"ipaddress" xml:"IPAddress"` Duration iso8601.Period `json:"duration" xml:"Duration"` } func (me Hoge) String() string { duration, _ := me.Duration.Duration() return fmt.Sprintf("%s %s", me.IPAddress.String(), duration.String()) } func main() { jb := []byte(` { "ipaddress":"192.168.0.1", "duration":"PT1440M" }`) var objFromJson Hoge json.Unmarshal(jb, &objFromJson) fmt.Printf("json:%s\n", objFromJson.String()) xb := []byte(` <?xml version="1.0"?> <Hoge> <IPAddress>192.168.0.1</IPAddress> <Duration>PT1440M</Duration> </Hoge> `) var objFromXml Hoge xml.Unmarshal(xb, &objFromXml) fmt.Printf("xml:%s\n", objFromJson.String()) } ``` net.IP型/period型のUnmarshalText()も ```go= ... json.Unmarshal(jb, &objFromJson) ... xml.Unmarshal(xb, &objFromXml) ``` で呼び出される。 実行結果 ``` json:192.168.0.1 24h0m0s xml:192.168.0.1 24h0m0s ```