-
Notifications
You must be signed in to change notification settings - Fork 919
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 { | ||
|
Contributor
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?
Collaborator
Author
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) | ||
|
Contributor
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
Collaborator
Author
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
Contributor
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.MinKeyorprimitive.MaxKey, you lose the special BSON marshaler behavior for theprimitive.MinKeyandprimitive.MaxKeytypes. That means it's not possible to define a customUnmarshalBSONfunction for theprimitive.MinKeyorprimitive.MaxKeytypes while maintaining the special marshaling behavior (you can't define additional functions for theprimitive.MinKeyorprimitive.MaxKeytypes outside of theprimitivepackage).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
myMinKeybased onprimitive.MinKeyand 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
UnmarshalBSONfunction for theprimitive.MinKey/primitive.MaxKeytypes 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.MinKeyorprimitive.MaxKeychanges the marshal behavior. Thanks for the explanation!