リストまたは値のいずれかを取るYAMLの読み込み

Posted: , Modified:   Go YAML Qiita

本稿は Qiita 投稿記事 のバックアップです.

概要

通常はリストを取るが,要素が一つしかない場合はリストにせず値を直接書いて良い, というフォーマットの YAML 文書を読み込む方法.

次の文書のように author はリストを受け取るのだが,

title: some book
author:
  - Alice
  - Bob

著者が一人しかいない場合は,

title: some book
author:
  - Alice

title: some book
author: Alice

のどちらでも OK というような場合を考える. この時,次のような構造体を用意して,

type Book struct {
    Title string
    Author []string
}

yaml.Unmarshal で次のように読み込もうとすると,author がリストになっていない場合にエラーになる.

t := &Book{}

// data は bool.yml のバイト列
err := yaml.Unmarshal([]byte(data), t)
if err != nil {
  log.Fatalf("error: %v", err)
}

解決策

Author の型が string と []string の二種類の場合があることが問題なので, まずは一般的な interface{} で受けておいて,後で変換することにする.

補助の構造体を使って読み込むには,Unmarshalerを実装すれば良いらしい.(参考:Decoding YAML in Go

まず, Unmarshaler を適用させたい項目用の型を定義する.

type ListOrString []string

type Book struct {
    Title string
    Author ListOrString
}

次に,この ListOrString 型に Unmarshaler を実装させる.

func (e *ListOrString) UnmarshalYAML(unmarshal func(interface{}) error) (err error) {

    var aux interface{}
    if err = unmarshal(&aux); err != nil {
        return
    }

    switch raw := aux.(type) {
    case string:
        *e = []string{raw}

    case []interface{}:
        list := make([]string, len(raw))
        for i, r := range raw {
            v, ok := r.(string)
            if !ok {
                return fmt.Errorf("An item in evn cannot be converted to a string: %v", aux)
            }
            list[i] = v
        }
        *e = list

    }
    return
}

UnmarshalYAML の中では,unmarshal関数を使って中間的な構造体(aux)に値を読み込ませる. その後,必要な変換を行なって,目的の変数(*e)に値を保存する.