Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions bson/decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,13 @@ func (d *Decoder) DefaultDocumentM() {
d.dc.defaultDocumentType = reflect.TypeOf(M{})
}

// DefaultDocumentMap causes the Decoder to always unmarshal documents into the
// map[string]any type. This behavior is restricted to data typed as "any" or
// "map[string]any".
func (d *Decoder) DefaultDocumentMap() {
d.dc.defaultDocumentType = reflect.TypeOf(map[string]any{})
}

// AllowTruncatingDoubles causes the Decoder to truncate the fractional part of BSON "double" values
// when attempting to unmarshal them into a Go integer (int, int8, int16, int32, or int64) struct
// field. The truncation logic does not apply to BSON "decimal128" values.
Expand Down
45 changes: 45 additions & 0 deletions bson/decoder_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,51 @@ func ExampleDecoder_DefaultDocumentM() {
// Output: {"Name":"New York","Properties":{"elevation":10,"population":8804190,"state":"NY"}}
}

func ExampleDecoder_DefaultDocumentMap() {
// Marshal a BSON document that contains a city name and a nested document
// with various city properties.
doc := bson.D{
{Key: "name", Value: "New York"},
{Key: "properties", Value: bson.D{
{Key: "state", Value: "NY"},
{Key: "population", Value: 8_804_190},
{Key: "elevation", Value: 10},
}},
}
data, err := bson.Marshal(doc)
if err != nil {
panic(err)
}

// Create a Decoder that reads the marshaled BSON document and use it to unmarshal the document
// into a City struct.
decoder := bson.NewDecoder(bson.NewDocumentReader(bytes.NewReader(data)))

type City struct {
Name string `bson:"name"`
Properties any `bson:"properties"`
}

// Configure the Decoder to default to decoding BSON documents as a
// map[string]any type if the decode destination has no type information. The
// Properties field in the City struct will be decoded as map[string]any
// instead of the default "D".
decoder.DefaultDocumentMap()

var res City
err = decoder.Decode(&res)
if err != nil {
panic(err)
}

data, err = json.Marshal(res)
if err != nil {
panic(err)
}
fmt.Printf("%+v\n", string(data))
// Output: {"Name":"New York","Properties":{"elevation":10,"population":8804190,"state":"NY"}}
}

func ExampleDecoder_UseJSONStructTags() {
// Marshal a BSON document that contains the name, SKU, and price (in cents)
// of a product.
Expand Down
41 changes: 41 additions & 0 deletions bson/decoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,23 @@ func TestDecoderConfiguration(t *testing.T) {
{Key: "myDocument", Value: M{"myString": "test value"}},
},
},
// Test that DefaultDocumentMap always decodes BSON documents into
// map[string]any values, independent of the top-level Go value type.
{
description: "DefaultDocumentMap nested",
configure: func(dec *Decoder) {
dec.DefaultDocumentMap()
},
input: bsoncore.NewDocumentBuilder().
AppendDocument("myDocument", bsoncore.NewDocumentBuilder().
AppendString("myString", "test value").
Build()).
Build(),
decodeInto: func() any { return &D{} },
want: &D{
{Key: "myDocument", Value: map[string]any{"myString": "test value"}},
},
},
// Test that ObjectIDAsHexString causes the Decoder to decode object ID to hex.
{
description: "ObjectIDAsHexString",
Expand Down Expand Up @@ -697,6 +714,30 @@ func TestDecoderConfiguration(t *testing.T) {
}
assert.Equal(t, want, got, "expected and actual decode results do not match")
})
t.Run("DefaultDocumentMap top-level", func(t *testing.T) {
t.Parallel()

input := bsoncore.NewDocumentBuilder().
AppendDocument("myDocument", bsoncore.NewDocumentBuilder().
AppendString("myString", "test value").
Build()).
Build()

dec := NewDecoder(NewDocumentReader(bytes.NewReader(input)))

dec.DefaultDocumentMap()

var got any
err := dec.Decode(&got)
require.NoError(t, err, "Decode error")

want := map[string]any{
"myDocument": map[string]any{
"myString": "test value",
},
}
assert.Equal(t, want, got, "expected and actual decode results do not match")
})
t.Run("Default decodes DocumentD for top-level", func(t *testing.T) {
t.Parallel()

Expand Down
27 changes: 27 additions & 0 deletions docs/migration-2.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,33 @@ fmt.Printf("b3.b type: %T\n", b3["b"])

Use `Decoder.DefaultDocumentM()` or set the `DefaultDocumentM` field of `options.BSONOptions` to always decode documents into the `bson.M` type.

For full V1 compatibility, use `Decoder.DefaultDocumentMap()` instead. While
`bson.M` is defined as `type M map[string]any`, Go's type system treats `bson.M`
and `map[string]any` as distinct types. This can break compatibility with
libraries that expect actual `map[string]any` types.

```go
b1 := map[string]any{"a": 1, "b": map[string]any{"c": 2}}
b2, _ := bson.Marshal(b1)

decoder := bson.NewDecoder(bson.NewDocumentReader(bytes.NewReader(b2)))
decoder.DefaultDocumentMap()

var b3 map[string]any
decoder.Decode(&b3)
fmt.Printf("b3.b type: %T\n", b3["b"])
// Output: b3.b type: map[string]interface {}
```

Or configure at the client level:

```go
clientOpts := options.Client().
SetBSONOptions(&options.BSONOptions{
DefaultDocumentMap: true,
})
```

#### NewDecoder

The signature of `NewDecoder` has been updated without an error being returned.
Expand Down
7 changes: 5 additions & 2 deletions mongo/cursor.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,9 @@ func getDecoder(
if opts.DefaultDocumentM {
dec.DefaultDocumentM()
}
if opts.DefaultDocumentMap {
dec.DefaultDocumentMap()
}
if opts.ObjectIDAsHexString {
dec.ObjectIDAsHexString()
}
Expand Down Expand Up @@ -423,8 +426,8 @@ func (c *Cursor) RemainingBatchLength() int {
// addFromBatch adds all documents from batch to sliceVal starting at the given index. It returns the new slice value,
// the next empty index in the slice, and an error if one occurs.
func (c *Cursor) addFromBatch(sliceVal reflect.Value, elemType reflect.Type, batch *bsoncore.Iterator,
index int) (reflect.Value, int, error) {

index int,
) (reflect.Value, int, error) {
docs, err := batch.Documents()
if err != nil {
return sliceVal, index, err
Expand Down
5 changes: 5 additions & 0 deletions mongo/options/clientoptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,11 @@ type BSONOptions struct {
// "any" or "map[string]any".
DefaultDocumentM bool

// DefaultDocumentMap causes the driver to always unmarshal documents into the
// map[string]any type. This behavior is restricted to data typed as "any" or
// "map[string]any".
DefaultDocumentMap bool

// ObjectIDAsHexString causes the Decoder to decode object IDs to their hex
// representation.
ObjectIDAsHexString bool
Expand Down
Loading