Go である文字列が XML・JSON の形式になっているかどうかを調べたいとき
はじめに
Go 言語で文字列から XML や JSON の情報を構造体へ変換するのはとても簡単です。
また、その逆もしかりです。
ビルトインの機能である xml
, json
を import して、
変換する構造体を作成し、Unmarshal
すれば文字列を構造体へ変換することができます。
(構造体から文字列に変換するには Marshal
を使えば可能です。)
ところで、XML・JSON の情報自体は不要なんだけど、
文字列がそれらのフォーマットになっているかどうかは判定したい場合があると思います。
まぁ、私自身ですらそういうケースはほぼないんですけど。
ただ、どういうケースで必要と感じたかというと、別のAPIサーバーのレスポンスを取得し、
その情報を加工せずに別のサーバーへのレスポンスとして渡す場合です。
取得元のサーバーで正しくバリデーションされていれば、そのまま返却すればいいのですが、
レスポンスとして渡す以上、正常な状態かどうかは確認しておきたいという欲求にかられます。
(それでもちゃんとI/Fの仕様を決めて・把握してバリデーションするのがベターだとは思いますが)
以上のことを実現したかったのですが、どうすればよいかすぐ思い当たらなかったので、
自分がやった方法を記載します。
後ほど動作確認しますが、確認した Go のバージョンは go1.16
で行いました。
内容
実現方法
無名の構造体を Unmarshal
する構造体に使うと、文字列が期待したフォーマットかどうか判断できるかと思われます。
- XML の場合
func isParsableXml(value string) bool {
if err := xml.Unmarshal([]byte(value), &struct{}{}); err != nil {
fmt.Printf("failed to paese xml, value = [%s], reason = [%s]\n", value, err.Error())
return false
}
return true
}
- JSON の場合
func isParsableJson(value string) bool {
if err := json.Unmarshal([]byte(value), &struct {}{}); err != nil {
fmt.Printf("failed to paese json, value = [%s], reason = [%s]\n", value, err.Error())
return false
}
return true
}
動作確認
本当に文字列が期待したフォーマットの時は成功するのか、
異常フォーマットの場合はエラーになるのかを確認します。
testting
を使ったユニットテストで動作確認しました。
package parser
import "testing"
func Test_isParsableJson(t *testing.T) {
type args struct {
value string
}
tests := []struct {
name string
args args
want bool
}{
{
name: "正常なJSON",
args: args{
value: `{
"sample": 1
}`,
},
want: true,
},
{
name: "正常なJSON. 空のJSONの場合",
args: args{
value: `{}`,
},
want: true,
},
{
name: "異常なJSON、value がない",
args: args{
value: `{"invalid"}`,
},
want: false,
},
{
name: "異常なJSON、空文字の場合",
args: args{
value: "",
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isParsableJson(tt.args.value); got != tt.want {
t.Errorf("isParsableJson() = %v, want %v", got, tt.want)
}
})
}
}
func Test_isParsableXml(t *testing.T) {
type args struct {
value string
}
tests := []struct {
name string
args args
want bool
}{
{
name: "正常なXML",
args: args{
value: `<?xml version="1.0"?><Sample>value_</Sample>`,
},
want: true,
},
{
name: "異常なXML.要素違いで閉じていない",
args: args{
value: `<?xml version="1.0"?>
<Sample>value_</Sample1>`,
},
want: false,
},
{
name: "異常なXML. 空文字の場合",
args: args{
value: "",
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isParsableXml(tt.args.value); got != tt.want {
t.Errorf("isParsableXml() = %v, want %v", got, tt.want)
}
})
}
}
長くなりましたが、「正常なフォーマット」、「それっぽいけど異常なフォーマット」、「空文字」の場合などをざっと調べています。
実行結果はこんな感じです
=== RUN Test_isParsableJson
=== RUN Test_isParsableJson/正常なJSON
=== RUN Test_isParsableJson/正常なJSON._空のJSONの場合
=== RUN Test_isParsableJson/異常なJSON、value_がない
failed to paese json, value = [{"invalid"}], reason = [invalid character '}' after object key]
=== RUN Test_isParsableJson/異常なJSON、空文字の場合
failed to paese json, value = [], reason = [unexpected end of JSON input]
--- PASS: Test_isParsableJson (0.00s)
--- PASS: Test_isParsableJson/正常なJSON (0.00s)
--- PASS: Test_isParsableJson/正常なJSON._空のJSONの場合 (0.00s)
--- PASS: Test_isParsableJson/異常なJSON、value_がない (0.00s)
--- PASS: Test_isParsableJson/異常なJSON、空文字の場合 (0.00s)
=== RUN Test_isParsableXml
=== RUN Test_isParsableXml/正常なXML
=== RUN Test_isParsableXml/異常なXML.要素違いで閉じていない
failed to paese xml, value = [<?xml version="1.0"?>
<Sample>value_</Sample1>], reason = [XML syntax error on line 2: element <Sample> closed by </Sample1>]
=== RUN Test_isParsableXml/異常なXML._空文字の場合
failed to paese xml, value = [], reason = [EOF]
--- PASS: Test_isParsableXml (0.00s)
--- PASS: Test_isParsableXml/正常なXML (0.00s)
--- PASS: Test_isParsableXml/異常なXML.要素違いで閉じていない (0.00s)
--- PASS: Test_isParsableXml/異常なXML._空文字の場合 (0.00s)
PASS
Process finished with the exit code 0
すべて期待通りに動いてますし、
フォーマット異常の場合は parse で false が返っていて、
error 文をみればなぜ失敗したかわかる内容になっているかと思います。
おまけ:没にした案
「1要素だけ分解してフィールドにもつ構造体をつくる」とかも考えました。
// <TopLevel>value</TopLevel> とかを XML を受け取る
type SampleStructure struct {
top string `xml:"TopLevel"`
}
これでも構造解析の有無は判定できるので、やりたいことはできるのですが、
なぜ没にしたかというと、構造体をつくったり、インスタンス化しても、
結局後の処理で使わないので、もったいないかなと思ったためです。
(ログ出力として使うのであればありな気はしました)
最後に
空構造体を Unmarshal
時に使えばやりたいことができそうということがわかりました。
ざっと動かした限りでは、問題もなく期待動作を得られることがわかりました。
Discussion