Try   HackMD

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より

■ 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より

文字列フィールドを文字列としてパースする場合

まず json/XMLに含まれる文字列フィールドを文字列としてパースする方法をおさらいしておく。
一般的に以下のように書く。
(エラーハンドリングは省略。以降同じ。)

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型として読むように修正したい。

... type Hoge struct { IPAddress string `json:"ipaddress" xml:"IPAddress"` Duration string `json:"duration" xml:"Duration"` } ...

方法1. UnmarshalJSON()/UnmarshalXML()を使う

IPアドレス文字列は以下の方法でnet.IP型にパースできる。

import ("net")
...
ip := net.ParseIP("192.168.0.1")

ISO8601継続時間文字列はそのままではtime.Duration型にパースできない。
ISO8601継続時間型を扱うライブラリ(ここでは"github.com/rickb777/date/period")を使えば以下の方法でtime.Duration型にパースできる。

import (iso8601 "github.com/rickb777/date/period")
...
iso8601duration, _ := iso8601.Parse("PT1440M")
duration, _ = iso8601duration.Duration()

上記をjson/XMLを構造体に読み込む際に呼び出されるメソッドUnmarshalJSON()/UnmarshalXML()に組み込む。

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()は

...
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)

今回のjson/XML構造体内のフィールドのnet.IP型はUnmarshalText()を持つ。(goDoc)
time.Durationはそれ自体ではISO8601継続時間文字列をパースできないので、time.Duration型に変わって、"github.com/rickb777/date/period"のperiod型をフィールドとして持たせる。
このperiod型はUnmarshalText()を持つ。(goDoc)

よって、方法1の場合とは違って

  • json/XMLそれぞれにUnmarshalJSON()/UnmarshalXML()のようなメソッドを用意する必要がない。
  • 各フィールドのためのパースメソッド(例:net.ParseIP())を呼び出す必要がない。
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()も

... json.Unmarshal(jb, &objFromJson) ... xml.Unmarshal(xb, &objFromXml)

で呼び出される。

実行結果

json:192.168.0.1 24h0m0s
xml:192.168.0.1 24h0m0s