27
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Go4Advent Calendar 2019

Day 10

mapstructureを使って複雑な構造のJSONを構造体にマッピングする

Last updated at Posted at 2019-12-10

この記事は、Go4 Advent Calendar 10日目の記事です。

先日、ネットサーフィンをしていたところ、SmartHRさんが公開している郵便番号から住所を引くことが出来るJSONデータのリポジトリを見つけました。
https://github.com/kufu/jpostcode-data

このデータをGoで扱いたかったのでencoding/jsonでUnmarshalすることを試みたのですが、
簡単には通らなかったので色々調査を行い、最終的にstructにマッピングすることに成功しました。

jpostcode-dataに格納されているJSONの形式

jpostcode-dataのJSONデータは、下記のような構成になっていました。

  • 郵便番号の先頭3桁をファイル名としたJSONファイルが1ディレクトリ配下に設置されている
    • "001.json", "002.json", ...
  • JSONの内容は1つの巨大なObject
    • 郵便番号の末尾4桁をキーに、住所を値にセットしている。
001.json
{
  "0000": {
    "postcode": "0010000",
    "prefecture": "北海道",
    "prefecture_kana": "ホッカイドウ",
    "prefecture_code": 1,
    "city": "札幌市北区",
    "city_kana": "サッポロシキタク",
    "town": "",
    "town_kana": "",
    "street": null,
    "office_name": null,
    "office_name_kana": null
  },
  "0010": {
    "postcode": "0010010",
    "prefecture": "北海道",
    "prefecture_kana": "ホッカイドウ",
    ...
  },
  ...
}

このような構造の場合、JSON全体を stringをkeyとしたmap と見立てて、値をマッピングするための構造体を定義し、encoding/jsonでUnmarshalすればよい、と方針を立てると思います。

Address構造体の定義

json structタグでJSON Objectの各キーに構造体のフィールドを対応させます。

type Address struct {
	PostCode       string `json:"post_code"`
	Prefecture     string `json:"prefecture"`
	PrefectureKana string `json:"prefecture_kana"`
	PrefectureCode int    `json:"prefecture_code"`
	City           string `json:"city"`
	CityKana       string `json:"city_kana"`
	Town           string `json:"town"`
	TownKana       string `json:"town_kana"`
	Street         string `json:"street"`
	OfficeName     string `json:"office_name"`
	OfficeNameKana string `json:"office_name_kana"`
}

Address構造体へのマッピング

map[string]Address 型の変数を宣言して、そこにマッピングを行います。

f, err := os.Open("001.json")
...
var data map[string]Address
if err := json.NewDecoder(f).Decode(&data); err != nil {
  return err
}
fmt.Printf("%#v\n", data["0000"])

001.json については、期待通り簡単にGoのmap + Address構造体にマッピングすることが出来ました。

Unmarshalに失敗するパターンの存在

これでうまくいったかと思いきや、Unmarshalに失敗するパターンが存在することがわかりました。
これは上記に示したコードで 011.json を読み込もうと思った時のエラーメッセージです。

2019/12/10 10:33:43 json: cannot unmarshal array into Go value of type main.Address
exit status 1

配列をAddress構造体にマッピング出来ない。とのことです。なるほど…。

そこで元のJSONデータを見てみると、値としてObjectではなく、配列が指定されているパターンがあることがわかりました。
具体的には、下記のような一つの郵便番号に対して複数の町名が存在するものです。

011.json
{
  ...,
  "0951": [
    {
      "postcode": "0110951",
      "prefecture": "秋田県",
      ...,
      "city": "秋田市",
      ...,
      "town": "土崎港相染町",
      ...
    },
    {
      "postcode": "0110951",
      "prefecture": "秋田県",
      ...,
      "city": "秋田市",
      ...,
      "town": "土崎港古川町",
      ...
    }
  ],
  "8501": {
    "postcode": "0118501",
    "prefecture": "秋田県",
    "prefecture_kana": "アキタケン",
    ...
  },
  ...
}

このように、値に複数の形式が存在する場合は、Goのencoding/jsonで単純にマッピングを行うことは出来ません。
対処法としては、まずinterface{}型にUnmarshalした上で、実際の型を検査して分岐するといったものになります。

interface{}にUnmarshalする

先ほどは、 map[string]Address 型の変数に対してUnmarshalを行ったので失敗していたのですが、
これを map[string]interface{} へのUnmarshalにした場合の挙動はどうなるでしょうか?

結論としては、

  • 値がObjectであればmap[string]interface{}に
  • 値が配列であれば[]interface{}に

といった形でマッピングされます。
(詳細は公式ドキュメントを参照ください: https://golang.org/pkg/encoding/json/#Unmarshal )

interface{}型へのUnmarshalにしてしまえばコケることはありませんが、
これでは全てのObjectがmapになってしまうので、encoding/jsonの json 構造体タグを使ったフィールドへのマッピングは出来なくなってしまいます。

mapstructureでmapを構造体にマッピングする

そこで登場するのがmapstructureです。
https://github.com/mitchellh/mapstructure

mapstructureを使えば、mapを構造体にマッピングすることが出来ます。
先ほど示したObjectと配列が混在したデータ構造の場合も、該当箇所をinterface{}型にUnmarshalしてしまえば、Objectの値がmapとして保持できるので構造体にマッピング可能となります。

実際の使い方としては、下記のような関数を作っておいて、 mapstructure.Decode した結果を返すようにすればよいです。

func convertJSONToAddress(input interface{}) (Address, error) {
	var addr Address
	err := mapstructure.Decode(input, &addr)
	if err != nil {
		return Address{}, err
	}
	return addr, nil
}

mapstructureは、自動的にmapのキー名に対応する構造体のフィールド名があればマッピングを行ってくれるのですが、
今回、mapのキーにsnake_caseのものがあるため、あらかじめ mapstructure の構造体タグを付与しておく必要がある点に注意が必要です。

type Address struct {
	PostCode       string `json:"post_code" mapstructure:"post_code"`
	Prefecture     string `json:"prefecture" mapstructure:"prefecture"`
	PrefectureKana string `json:"prefecture_kana" mapstructure:"prefecture_kana"`
	PrefectureCode int    `json:"prefecture_code" mapstructure:"prefecture_code"`
	City           string `json:"city" mapstructure:"city"`
	CityKana       string `json:"city_kana" mapstructure:"city_kana"`
	Town           string `json:"town" mapstructure:"town"`
	TownKana       string `json:"town_kana" mapstructure:"town_kana"`
	Street         string `json:"street" mapstructure:"street"`
	OfficeName     string `json:"office_name" mapstructure:"office_name"`
	OfficeNameKana string `json:"office_name_kana" mapstructure:"office_name_kana"`
}

完成版のコードは下記の通りです。
最終的に、Objectと配列が混じっていたものを、統一的にsliceに格納する形で addressesMap を作成するようにしました。

func main() {
	f, err := os.Open("011.json")
	defer f.Close()
	if err != nil {
		log.Fatalf("%v", err)
	}
	addressesMap := map[string][]Address{}
	var rawDataMap map[string]interface{}
	if err := json.NewDecoder(f).Decode(&rawDataMap); err != nil {
		log.Fatalf("%v", err)
	}
	for k, v := range rawDataMap {
		switch reflect.TypeOf(v).Kind() {
		case reflect.Slice:
			rawAddrs, ok := v.([]interface{})
			if !ok {
				continue
			}
			for _, rawAddr := range rawAddrs {
				addr, err := convertJSONToAddress(rawAddr)
				if err != nil {
					log.Fatalf("%v", err)
				}
				addressesMap[k] = append(addressesMap[k], addr)
			}
		default:
			addr, err := convertJSONToAddress(v)
			if err != nil {
				log.Fatalf("%v", err)
			}
			addressesMap[k] = append(addressesMap[k], addr)
		}
	}
	fmt.Printf("%#v\n", addressesMap["0951"])
}

最後に

ちょっと複雑そうな形式のJSONも、mapstructureを使えばうまくマッピング出来るパターンがあるので、似たようなシーンに遭遇したらぜひ思い出してみてください!

今回の成果物はこちらのリポジトリに置いています。
https://github.com/syumai/go-jpostcode
Goで郵便番号 => 住所取得が簡単に行えるpackageとなっています。(サイズはめちゃくちゃ大きいです…汗)

27
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
27
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?