🦔

Go である文字列が XML・JSON の形式になっているかどうかを調べたいとき

2021/07/28に公開

はじめに

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