# 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
```