Skip to content
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

GODRIVER-1765 Improve JSON marshaling/unmarshaling for bson.D, bson.M and bson.A. #1594

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

qingyang-hu
Copy link
Collaborator

GODRIVER-1765

Summary

This PR adds json.Marshaler and json.Unmarshaler for primitive.D, so it can handle JSON objects properly.
The PR also adds json.Unmarshalers for primitive.M and primitive.A, so JSON strings can be unmarshaled into nested structures correspondingly.

After the "primitive" merging into "bson" package, we can migrate these primitive methods to their bson types.

Copy link

API Change Report

./bson/primitive

compatible changes

(*A).UnmarshalJSON: added
(*D).UnmarshalJSON: added
(*M).UnmarshalJSON: added
D.MarshalJSON: added

prestonvasquez
prestonvasquez previously approved these changes Apr 15, 2024
Copy link
Collaborator

@prestonvasquez prestonvasquez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

Comment on lines 241 to 258
dec := json.NewDecoder(bytes.NewReader(b))
t, err := dec.Token()
if err != nil {
return err
}
if t == nil {
*d = nil
return nil
}
if v, ok := t.(json.Delim); !ok || v != '{' {
return &json.UnmarshalTypeError{
Value: tokenString(t),
Type: reflect.TypeOf(*d),
Offset: dec.InputOffset(),
}
}
*d, err = jsonDecodeD(dec)
return err
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Optional] You could combine the logic in D.UnmarshalJSON and m.UnmarshalJSON using generics:

func newJsonObjDecoder[T D | M](b []byte, val *T) (*json.Decoder, error) {
	dec := json.NewDecoder(bytes.NewReader(b))

	t, err := dec.Token()
	if err != nil {
		return nil, err
	}

	if t == nil {
		*val = nil
		return nil, nil
	}

	if v, ok := t.(json.Delim); !ok || v != '{' {
		return nil, &json.UnmarshalTypeError{
			Value:  tokenString(t),
			Type:   reflect.TypeOf(*val),
			Offset: dec.InputOffset(),
		}
	}

	return dec, nil
}

}
}
buf.Write([]byte("}"))
return json.RawMessage(buf.Bytes()).MarshalJSON()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the purpose of returning the bytes via json.RawMessage().MarshalJSON here rather than returning them directly (i.e. return buf.Bytes(), nil)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +477 to +479
b, _ := json.Marshal(tc.test)
var got D
err := json.Unmarshal(b, &got)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Testing that JSON unmarshaling works with a document marshaled by the custom JSON marshaler can hide bugs in the custom JSON unmarshaling logic. For example, if the custom JSON marshaling code always uses a semicolon instead of a colon for a key/value separator, the JSON unmarshaling code could incorrectly expect a semicolon separator, but the tests would still pass.

We should use a known-correct JSON document (e.g. a string literal, embedded string from a text doc, or generated with a trusted reference) to test the JSON unmarshaling logic. That recommendation applies to all Unmarshal tests, but is most relevant here because both marshaling and unmarshaling are custom.

}
}

func TestUnmarshalingD(t *testing.T) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: Consider a more descriptive test name (borrowed from testable examples name conventions).

Suggested change
func TestUnmarshalingD(t *testing.T) {
func TestD_UnmarshalJSON(t *testing.T) {

@@ -199,3 +200,334 @@ func TestDateTime(t *testing.T) {
})
})
}

func TestMarshalingD(t *testing.T) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: Consider a more descriptive test name (borrowed from testable examples name conventions).

Suggested change
func TestMarshalingD(t *testing.T) {
func TestD_MarshalJSON(t *testing.T) {

}
}

func TestUnmarshalingA(t *testing.T) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: Consider a more descriptive test name (borrowed from testable examples name conventions).

Suggested change
func TestUnmarshalingA(t *testing.T) {
func TestA_UnmarshalJSON(t *testing.T) {

}
}

func TestUnmarshalingM(t *testing.T) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: Consider a more descriptive test name (borrowed from testable examples name conventions).

Suggested change
func TestUnmarshalingM(t *testing.T) {
func TestM_UnmarshalJSON(t *testing.T) {

@@ -223,9 +265,201 @@ type E struct {
// bson.M{"foo": "bar", "hello": "world", "pi": 3.14159}
type M map[string]interface{}

// UnmarshalJSON decodes M from JSON.
func (m *M) UnmarshalJSON(b []byte) error {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to define custom JSON unmarshaling logic for bson.M? Other than the type names being different, the JSON unmarshaling logic seems identical.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The custom unmarshaling for bson.M and bson.A is to handle nested structures correspondingly rather than simply unmarshal into interface{} as case tested at https://github.com/mongodb/mongo-go-driver/pull/1594/files#diff-1649d029623b36c04e8a0636bd65aab5bb1047410b1affb26206198a34a4c784R374-R381

// An A is an ordered representation of a BSON array.
//
// Example usage:
//
// bson.A{"bar", "world", 3.14159, bson.D{{"qux", 12345}}}
type A []interface{}

// UnmarshalJSON decodes A from JSON.
func (a *A) UnmarshalJSON(b []byte) error {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to define custom JSON unmarshaling logic for bson.A? Other than the type names being different, the JSON unmarshaling logic seems identical.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
priority-3-low Low Priority PR for Review
Projects
None yet
3 participants