2時に寝て変な夢をみて何度か起きて7時に起きた。いつも通りな感じ。

go のシリアライズ/デシリアライズとポインタ

ldap の distinguished name (以下dn) をパース するときはエスケープを扱う必要があるのでわりとややこしいのでライブラリを使った方がよいことを前回学んだ。

複数の web api で dn を扱っていると、それぞれで dn をパースして正規化した形で扱わないと大文字小文字の表記揺れなどに対応できなくて困るということに気付いた。いや、前回もうっすら気付いていたのだけど、既存の api はすべて対応しているからいいかなと楽観的に考えていた。すると、たまたま新規に dn を扱う web api を作ったときに正規化を忘れていることに気付いた。今後の保守や拡張を考慮すると、dn を string 型として扱うのは潜在的に正規化漏れの懸念があることからよくないと理解できた。そのため、既存のリクエストで受け取る dn を特別な型として必ず正規化して扱えるようにリファクタリングすることにした。あちこち直す必要はあったが、幸いにも単体テストも結合テストもそこそこあるのでバグっていればテストが落ちることで不具合には気付けるようになっていた。

encoding/json に Marshaler/Unmarshaler のインターフェースが定義されているのでそれぞれのメソッドを実装する必要がある。DNParameter の値を json にシリアライズするときは値レシーバで MarshalJSON メソッドを実装し、デシリアライズするときはポインタレシーバで UnmarshalJSON メソッドを実装しないと json ライブラリで意図した振る舞いにならないようにみえる。ここで UnmarshalJSON するときに byte 列から一旦 json の文字列に変換 (引用符を外す) してから ldap.ParseDN() しないといけない処理を直接 string 型に変換する誤ったコードを書いてしまって、この誤りに気付くのに1-2時間はまってしまった。

  • 誤ったコード
func (p *DNParameter) UnmarshalJSON(data []byte) error {
	dn, err := ldap.ParseDN(string(data))
	if err == nil {
		(*p).Value = dn
	}
	return err
  • 正しいコード
func (p *DNParameter) UnmarshalJSON(data []byte) error {
	var s string
	if err := json.Unmarshal(data, &s); err != nil {
		return fmt.Errorf("dn should be a string")
	}
	dn, err := ldap.ParseDN(s)
	if err == nil {
		(*p).Value = dn
	}
	return err

このときに引用符を ldap のパーサーがエスケープした形で扱えてしまい、テストは失敗するけれど、見た目がほとんど同じ文字列で動いてしまうのにはまった。引用符がエスケープされてテストが落ちることには気付いたものの、どこの処理が問題なのかが分からなくてはまっていた。おそらくスクラッチからこの仕様でコードを書いていたらすぐに気付いたと思えるが、リファクタリングであちこち書き換えていたからどこの処理が誤っているのかの切り分けに時間がかかった。