-
Notifications
You must be signed in to change notification settings - Fork 909
GODRIVER-2252 Don't call UnmarshalBSON for pointer values if the BSON field value is empty. #833
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1504,6 +1504,18 @@ func (dvd DefaultValueDecoders) UnmarshalerDecodeValue(dc DecodeContext, vr bson | |
return err | ||
} | ||
|
||
// If the target Go value is a pointer and the BSON field value is empty, set the value to the | ||
// zero value of the pointer (nil) and don't call UnmarshalBSON. UnmarshalBSON has no way to | ||
// change the pointer value from within the function (only the value at the pointer address), | ||
// so it can't set the pointer to "nil" itself. Since the most common Go value for an empty BSON | ||
// field value is "nil", we set "nil" here and don't call UnmarshalBSON. This behavior matches | ||
// the behavior of the Go "encoding/json" unmarshaler when the target Go value is a pointer and | ||
// the JSON field value is "null". | ||
if val.Kind() == reflect.Ptr && len(src) == 0 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The check on L1505 checks whether the target value can be changed. Should this be moved below that block? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, that's a good idea. Will update. |
||
val.Set(reflect.Zero(val.Type())) | ||
return nil | ||
} | ||
|
||
fn := val.Convert(tUnmarshaler).MethodByName("UnmarshalBSON") | ||
errVal := fn.Call([]reflect.Value{reflect.ValueOf(src)})[0] | ||
if !errVal.IsNil() { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,64 +10,42 @@ import ( | |
"reflect" | ||
"testing" | ||
|
||
"github.com/google/go-cmp/cmp" | ||
"github.com/stretchr/testify/assert" | ||
"go.mongodb.org/mongo-driver/bson/bsoncodec" | ||
"go.mongodb.org/mongo-driver/bson/bsonrw" | ||
"go.mongodb.org/mongo-driver/internal/testutil/assert" | ||
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore" | ||
) | ||
|
||
func TestUnmarshal(t *testing.T) { | ||
for _, tc := range unmarshalingTestCases { | ||
for _, tc := range unmarshalingTestCases() { | ||
t.Run(tc.name, func(t *testing.T) { | ||
if tc.reg != nil { | ||
t.Skip() // test requires custom registry | ||
} | ||
got := reflect.New(tc.sType).Interface() | ||
err := Unmarshal(tc.data, got) | ||
noerr(t, err) | ||
if !cmp.Equal(got, tc.want) { | ||
t.Errorf("Did not unmarshal as expected. got %v; want %v", got, tc.want) | ||
} | ||
assert.Equal(t, tc.want, got, "Did not unmarshal as expected.") | ||
}) | ||
} | ||
} | ||
|
||
func TestUnmarshalWithRegistry(t *testing.T) { | ||
for _, tc := range unmarshalingTestCases { | ||
for _, tc := range unmarshalingTestCases() { | ||
t.Run(tc.name, func(t *testing.T) { | ||
var reg *bsoncodec.Registry | ||
if tc.reg != nil { | ||
reg = tc.reg | ||
} else { | ||
reg = DefaultRegistry | ||
} | ||
got := reflect.New(tc.sType).Interface() | ||
err := UnmarshalWithRegistry(reg, tc.data, got) | ||
err := UnmarshalWithRegistry(DefaultRegistry, tc.data, got) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since this uses the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I'd actually prefer to leave that there for coverage. Currently it's redundant, but there's nothing that says There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point. Leaving |
||
noerr(t, err) | ||
if !cmp.Equal(got, tc.want) { | ||
t.Errorf("Did not unmarshal as expected. got %v; want %v", got, tc.want) | ||
} | ||
assert.Equal(t, tc.want, got, "Did not unmarshal as expected.") | ||
}) | ||
} | ||
} | ||
|
||
func TestUnmarshalWithContext(t *testing.T) { | ||
for _, tc := range unmarshalingTestCases { | ||
for _, tc := range unmarshalingTestCases() { | ||
t.Run(tc.name, func(t *testing.T) { | ||
var reg *bsoncodec.Registry | ||
if tc.reg != nil { | ||
reg = tc.reg | ||
} else { | ||
reg = DefaultRegistry | ||
} | ||
dc := bsoncodec.DecodeContext{Registry: reg} | ||
dc := bsoncodec.DecodeContext{Registry: DefaultRegistry} | ||
got := reflect.New(tc.sType).Interface() | ||
err := UnmarshalWithContext(dc, tc.data, got) | ||
noerr(t, err) | ||
if !cmp.Equal(got, tc.want) { | ||
t.Errorf("Did not unmarshal as expected. got %v; want %v", got, tc.want) | ||
} | ||
assert.Equal(t, tc.want, got, "Did not unmarshal as expected.") | ||
}) | ||
} | ||
} | ||
|
@@ -80,9 +58,7 @@ func TestUnmarshalExtJSONWithRegistry(t *testing.T) { | |
err := UnmarshalExtJSONWithRegistry(DefaultRegistry, data, true, &got) | ||
noerr(t, err) | ||
want := teststruct{1} | ||
if !cmp.Equal(got, want) { | ||
t.Errorf("Did not unmarshal as expected. got %v; want %v", got, want) | ||
} | ||
assert.Equal(t, want, got, "Did not unmarshal as expected.") | ||
}) | ||
|
||
t.Run("UnmarshalExtJSONInvalidInput", func(t *testing.T) { | ||
|
@@ -165,9 +141,7 @@ func TestUnmarshalExtJSONWithContext(t *testing.T) { | |
dc := bsoncodec.DecodeContext{Registry: DefaultRegistry} | ||
err := UnmarshalExtJSONWithContext(dc, tc.data, true, got) | ||
noerr(t, err) | ||
if !cmp.Equal(got, tc.want) { | ||
t.Errorf("Did not unmarshal as expected. got %+v; want %+v", got, tc.want) | ||
} | ||
assert.Equal(t, tc.want, got, "Did not unmarshal as expected.") | ||
}) | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No suggested change, but noting for consideration.
The original example marshaled the value to BSON Null. This seems like the desired behavior when the Go value is a pointer type:
From https://bsonspec.org/spec.html there three values that would unmarshal with an empty value:
I do not know if there is any use case for unmarshaling a Min Key or Max Key.
If Min Key or Max Key were unmarshaled to a Go value with a pointer type, setting the target to nil seems reasonable.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a great find! I did some testing and found that if you define a custom type with base type
primitive.MinKey
orprimitive.MaxKey
, you lose the special BSON marshaler behavior for theprimitive.MinKey
andprimitive.MaxKey
types. That means it's not possible to define a customUnmarshalBSON
function for theprimitive.MinKey
orprimitive.MaxKey
types while maintaining the special marshaling behavior (you can't define additional functions for theprimitive.MinKey
orprimitive.MaxKey
types outside of theprimitive
package).For example, if you encode a struct containing a
*primitive.MinKey
, you get a BSON document with field type "\xff" (the "min key" field type):Notice the "\xff" before "min", which is the encoding for the "min key" type.
If you define a new type like
myMinKey
based onprimitive.MinKey
and encode a similar struct, the field is encoded as a "document" type instead of a "min key" type.Notice the "\x03" before "min", which is the BSON encoding for a "document" type. Also, the "\x05\x00\x00\x00" after "min" is the encoding for an empty document value, not the zero-length value of a "min key" type.
tldr: While the "min key" and "max key" BSON types use a zero-length value like the "null" type, a user cannot define a custom
UnmarshalBSON
function for theprimitive.MinKey
/primitive.MaxKey
types while maintaining the special marshaling/unmarshaling behavior, so the change here shouldn't impact those types.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hah, interesting. TIL that a custom type definition of
primitive.MinKey
orprimitive.MaxKey
changes the marshal behavior. Thanks for the explanation!