LoginSignup
0
1

More than 5 years have passed since last update.

bsonとjsonでnil値をMarshalする時の挙動が違う

Last updated at Posted at 2017-08-09

golangのライブラリでbson(gopkg.in/mgo.v2/bson)もjson(encoding/json)も基本的に同じ(もちろん、出力データフォーマットが構造化テキストかbinaryかという違いはありますが)挙動だろうと思っていましたが、やはり思い込みは良くないですね。

ライブラリの実装短いんだし読めよって話もありますが、それはまた次回

package main

import (
    "encoding/json"
    "fmt"
    "gopkg.in/mgo.v2/bson"
    //"labix.org/v2/mgo/bson"
)

type Person struct {
    Name     string
    Metadata map[string]string
}

func main() {
    person := Person{Name: "Yuki"}
    fmt.Printf("Is person.Metadata null: %s\n", person.Metadata == nil)

    bsonData, err := bson.Marshal(&person)
    if err != nil {
        panic(err)
    }
    personFromBson := Person{}
    err = bson.Unmarshal(bsonData, &personFromBson)
    if err != nil {
        panic(err)
    }
    fmt.Printf("Bson: %q\n", bsonData)
    fmt.Printf("Is personFromBson.Metadata null: %s\n", personFromBson.Metadata == nil)

    jsonData, err := json.Marshal(&person)
    if err != nil {
        panic(err)
    }
    personFromJson := Person{}
    err = json.Unmarshal(jsonData, &personFromJson)
    if err != nil {
        panic(err)
    }
    fmt.Printf("Json: %q\n", jsonData)
    fmt.Printf("Is personFromJson.Metadata null: %s\n", personFromJson.Metadata == nil)
}

このプログラムでは、MetadataというattributeをもつPerson structを用意し、Metadataにnilを入れたままjsonとbsonでMarshalしております。でMarshalしたバイナリ, 構造化テキストをUnmarshalしその結果がnilかどうかを調べております

Yuki-no-MacBook-Pro-17795:gobson ukinau$ go run mgobson.go
Is person.Metadata null: %!s(bool=true)
Bson: "#\x00\x00\x00\x02name\x00\x05\x00\x00\x00Yuki\x00\x03metadata\x00\x05\x00\x00\x00\x00\x00"
Is personFromBson.Metadata null: %!s(bool=false)
Json: "{\"Name\":\"Yuki\",\"Metadata\":null}"
Is personFromJson.Metadata null: %!s(bool=true)

上記を見ての通り、

  • bson の場合は、nil値を初期化してserialise
  • json の場合は、nil値をnullとしてそのままserialise

していることがわかります。

ただ、上記の結果からのみだと、Data -> Marshal -> Unmarshal -> Dataした結果を見てどこかで、nil値がmapで初期化されていることしか確認できないので、Marshalで出力したbsonが本当にnullでなく空のmap相当の値が入っているいるのかわからないです。なので、

bsonで確かに、{} mapが初期化されていて、null値でないことを確かめる

specificationを読んで出力されたbsonを読み解いていこうと思います。
http://bsonspec.org/spec.html

まず、下記が今回出力されたbsonデータです。上の結果よりコピーしてます。一部16進数がUTF-8として文字評価されているので、#やらYukiやらmetadataという文字列が入っていますがこれらはUTF-8の文字列になります。

"#\x00\x00\x00\x02name\x00\x05\x00\x00\x00Yuki\x00\x03metadata\x00\x05\x00\x00\x00\x00\x00"

別記事で全体像を含めた解説をもう少し詳しく書きますが(まぁ短いspecificationをただ、長ったらしく説明するだけですが)、今回注目すべきは後半の下記の記述です

\x03metadata\x00\x05\x00\x00\x00\x00\x00"

まずbsonでは、mapの各キーとバリューのpairをelementと呼びそのelementにもいくつか種類がありelementの種類によって、接頭子に使う値や後続する置くべき値が違います。

\03

ここではmetadataが、\03から始まるので、bsonのelementのspecificationから該当するelementの種類を探すと

"\x03" e_name document Embedded document

この種類に該当することがわかります。
e_nameがキーの名前のUTF-8文字列 + \x00 を示しその後にdocument(golangでmap)が続くと決められているので、documentを示すbinary文字列(リトルエンディアンで最初4byteがdocumentのサイズ、5byte目がdocumentの終了を示す値\x00)が入っています。なので、golang のbson(gopkg.in/mgo.v2/bson) ライブラリはMarshal時に、mapを初期化してbsonデータにしていることがわかります。

bsonというdata formatにそもそもnull値がないんじゃないか?

とすると、次に疑うのがそもそもbsonというデータフォーマットがnull値をサポートしていないんじゃないか?
いえ、そんなことはありません。

"\x0A" e_name Null value

上記、specificationからの引用ですがきちんとnull値というelementのもあるのbsonとしてはnullを指定することはできます。

golangのbson(gopkg.in/mgo.v2/bson)でnull値を含むbsonをdecodeすると?

出力時(Marshal)にnull値を含むbsonを出力しないので、単純な興味として、Unmarshalしてnull値としてloadできるのかなというのを試したいと思います。

Bsonを下記のように変えてみます

"\x1e\x00\x00\x00\x02name\x00\x05\x00\x00\x00Yuki\x00\0Ametadata\x00\x00"

上記の説明の通り、\03から始まるdictionaryを示すelementだったものを\0Aから始まるnullを示すelementに変えて、全体のbyte数が変わったので、一番先頭の\x23から5byte引いて\x1eにしてみました。
これをMarshalして、Metadataがnullかどうかを調べると

package main

import (
    "encoding/json"
    "fmt"
    "gopkg.in/mgo.v2/bson"
    //"labix.org/v2/mgo/bson"
)

type Person struct {
    Name     string
    Metadata map[string]string
}

func main() {
    bsonData = []byte("\x1e\x00\x00\x00\x02name\x00\x05\x00\x00\x00Yuki\x00\x0Ametadata\x00\x00")
    personFromBson := Person{}
    err := bson.Unmarshal(bsonData, &personFromBson)
    if err != nil {
        panic(err)
    }
    fmt.Printf("Is personFromBson.Metadata null: %s\n", personFromBson.Metadata == nil)
}

結果

Yuki-no-MacBook-Pro-17795:gobson ukinau$ go run mgobson.go
Is personFromBson.Metadata null: %!s(bool=true)

となるので、きちんとnull値をUnmarshalできていることがわかります。

結論と疑問

bsonデータを直接見ても分かる通り、golangのbsonライブラリはnilをnullと解釈せず、各typeで初期化してserialiseするようです。これは意図しているのか、実装上そうなったのかはまだ追えてませんが、個人的な感想としては、jsonライブラリの挙動に合わせていった方がわかりやすいなーと思います。

golangの勉強も兼ねて、bsonのライブラリは今度読んで見たいと思います。

0
1
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
0
1