Skip to content

Commit c383b84

Browse files
Add DefaultDocumentMap as Decoder Method
1 parent 9bd07db commit c383b84

File tree

6 files changed

+130
-2
lines changed

6 files changed

+130
-2
lines changed

bson/decoder.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,13 @@ func (d *Decoder) DefaultDocumentM() {
9393
d.dc.defaultDocumentType = reflect.TypeOf(M{})
9494
}
9595

96+
// DefaultDocumentMap causes the Decoder to always unmarshal documents into the
97+
// map[string]any type. This behavior is restricted to data typed as "any" or
98+
// "map[string]any".
99+
func (d *Decoder) DefaultDocumentMap() {
100+
d.dc.defaultDocumentType = reflect.TypeOf(map[string]any{})
101+
}
102+
96103
// AllowTruncatingDoubles causes the Decoder to truncate the fractional part of BSON "double" values
97104
// when attempting to unmarshal them into a Go integer (int, int8, int16, int32, or int64) struct
98105
// field. The truncation logic does not apply to BSON "decimal128" values.

bson/decoder_example_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,51 @@ func ExampleDecoder_DefaultDocumentM() {
9494
// Output: {"Name":"New York","Properties":{"elevation":10,"population":8804190,"state":"NY"}}
9595
}
9696

97+
func ExampleDecoder_DefaultDocumentMap() {
98+
// Marshal a BSON document that contains a city name and a nested document
99+
// with various city properties.
100+
doc := bson.D{
101+
{Key: "name", Value: "New York"},
102+
{Key: "properties", Value: bson.D{
103+
{Key: "state", Value: "NY"},
104+
{Key: "population", Value: 8_804_190},
105+
{Key: "elevation", Value: 10},
106+
}},
107+
}
108+
data, err := bson.Marshal(doc)
109+
if err != nil {
110+
panic(err)
111+
}
112+
113+
// Create a Decoder that reads the marshaled BSON document and use it to unmarshal the document
114+
// into a City struct.
115+
decoder := bson.NewDecoder(bson.NewDocumentReader(bytes.NewReader(data)))
116+
117+
type City struct {
118+
Name string `bson:"name"`
119+
Properties any `bson:"properties"`
120+
}
121+
122+
// Configure the Decoder to default to decoding BSON documents as a
123+
// map[string]any type if the decode destination has no type information. The
124+
// Properties field in the City struct will be decoded as map[string]any
125+
// instead of the default "D".
126+
decoder.DefaultDocumentMap()
127+
128+
var res City
129+
err = decoder.Decode(&res)
130+
if err != nil {
131+
panic(err)
132+
}
133+
134+
data, err = json.Marshal(res)
135+
if err != nil {
136+
panic(err)
137+
}
138+
fmt.Printf("%+v\n", string(data))
139+
// Output: {"Name":"New York","Properties":{"elevation":10,"population":8804190,"state":"NY"}}
140+
}
141+
97142
func ExampleDecoder_UseJSONStructTags() {
98143
// Marshal a BSON document that contains the name, SKU, and price (in cents)
99144
// of a product.

bson/decoder_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,23 @@ func TestDecoderConfiguration(t *testing.T) {
559559
{Key: "myDocument", Value: M{"myString": "test value"}},
560560
},
561561
},
562+
// Test that DefaultDocumentMap always decodes BSON documents into
563+
// map[string]any values, independent of the top-level Go value type.
564+
{
565+
description: "DefaultDocumentMap nested",
566+
configure: func(dec *Decoder) {
567+
dec.DefaultDocumentMap()
568+
},
569+
input: bsoncore.NewDocumentBuilder().
570+
AppendDocument("myDocument", bsoncore.NewDocumentBuilder().
571+
AppendString("myString", "test value").
572+
Build()).
573+
Build(),
574+
decodeInto: func() any { return &D{} },
575+
want: &D{
576+
{Key: "myDocument", Value: map[string]any{"myString": "test value"}},
577+
},
578+
},
562579
// Test that ObjectIDAsHexString causes the Decoder to decode object ID to hex.
563580
{
564581
description: "ObjectIDAsHexString",
@@ -697,6 +714,30 @@ func TestDecoderConfiguration(t *testing.T) {
697714
}
698715
assert.Equal(t, want, got, "expected and actual decode results do not match")
699716
})
717+
t.Run("DefaultDocumentMap top-level", func(t *testing.T) {
718+
t.Parallel()
719+
720+
input := bsoncore.NewDocumentBuilder().
721+
AppendDocument("myDocument", bsoncore.NewDocumentBuilder().
722+
AppendString("myString", "test value").
723+
Build()).
724+
Build()
725+
726+
dec := NewDecoder(NewDocumentReader(bytes.NewReader(input)))
727+
728+
dec.DefaultDocumentMap()
729+
730+
var got any
731+
err := dec.Decode(&got)
732+
require.NoError(t, err, "Decode error")
733+
734+
want := map[string]any{
735+
"myDocument": map[string]any{
736+
"myString": "test value",
737+
},
738+
}
739+
assert.Equal(t, want, got, "expected and actual decode results do not match")
740+
})
700741
t.Run("Default decodes DocumentD for top-level", func(t *testing.T) {
701742
t.Parallel()
702743

docs/migration-2.0.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -892,6 +892,33 @@ fmt.Printf("b3.b type: %T\n", b3["b"])
892892
893893
Use `Decoder.DefaultDocumentM()` or set the `DefaultDocumentM` field of `options.BSONOptions` to always decode documents into the `bson.M` type.
894894
895+
For full V1 compatibility, use `Decoder.DefaultDocumentMap()` instead. While
896+
`bson.M` is defined as `type M map[string]any`, Go's type system treats `bson.M`
897+
and `map[string]any` as distinct types. This can break compatibility with
898+
libraries that expect actual `map[string]any` types.
899+
900+
```go
901+
b1 := map[string]any{"a": 1, "b": map[string]any{"c": 2}}
902+
b2, _ := bson.Marshal(b1)
903+
904+
decoder := bson.NewDecoder(bson.NewDocumentReader(bytes.NewReader(b2)))
905+
decoder.DefaultDocumentMap()
906+
907+
var b3 map[string]any
908+
decoder.Decode(&b3)
909+
fmt.Printf("b3.b type: %T\n", b3["b"])
910+
// Output: b3.b type: map[string]interface {}
911+
```
912+
913+
Or configure at the client level:
914+
915+
```go
916+
clientOpts := options.Client().
917+
SetBSONOptions(&options.BSONOptions{
918+
DefaultDocumentMap: true,
919+
})
920+
```
921+
895922
#### NewDecoder
896923
897924
The signature of `NewDecoder` has been updated without an error being returned.

mongo/cursor.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,9 @@ func getDecoder(
320320
if opts.DefaultDocumentM {
321321
dec.DefaultDocumentM()
322322
}
323+
if opts.DefaultDocumentMap {
324+
dec.DefaultDocumentMap()
325+
}
323326
if opts.ObjectIDAsHexString {
324327
dec.ObjectIDAsHexString()
325328
}
@@ -423,8 +426,8 @@ func (c *Cursor) RemainingBatchLength() int {
423426
// addFromBatch adds all documents from batch to sliceVal starting at the given index. It returns the new slice value,
424427
// the next empty index in the slice, and an error if one occurs.
425428
func (c *Cursor) addFromBatch(sliceVal reflect.Value, elemType reflect.Type, batch *bsoncore.Iterator,
426-
index int) (reflect.Value, int, error) {
427-
429+
index int,
430+
) (reflect.Value, int, error) {
428431
docs, err := batch.Documents()
429432
if err != nil {
430433
return sliceVal, index, err

mongo/options/clientoptions.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,11 @@ type BSONOptions struct {
210210
// "any" or "map[string]any".
211211
DefaultDocumentM bool
212212

213+
// DefaultDocumentMap causes the driver to always unmarshal documents into the
214+
// map[string]any type. This behavior is restricted to data typed as "any" or
215+
// "map[string]any".
216+
DefaultDocumentMap bool
217+
213218
// ObjectIDAsHexString causes the Decoder to decode object IDs to their hex
214219
// representation.
215220
ObjectIDAsHexString bool

0 commit comments

Comments
 (0)