[GODRIVER-2141] Add a recursive option to Map() Created: 27/Aug/21  Updated: 27/Oct/23  Resolved: 20/Sep/21

Status: Closed
Project: Go Driver
Component/s: None
Affects Version/s: None
Fix Version/s: None

Type: New Feature Priority: Minor - P4
Reporter: Nathan Leniz Assignee: Benji Rewis (Inactive)
Resolution: Gone away Votes: 0
Labels: None
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified

Issue Links:
Related
related to GODRIVER-2684 Deprecate "bson.D.Map" Closed

 Description   

Requesting a similar function to Map() or an option that enables Map() to recursively convert bson.D to a map.

Due to ordering, bson.D is preferable to bson.M and we're using bson.D exclusively in the developing Go documentation. This type isn't ideal when demonstrating output to users since everything is prepended with "Key" and "Value".

The Map() function provides a workaround for simple documents but breaks down on documents with arrays of documents, as it doesn't convert them.

Example:

	var result bson.D
	err = coll.FindOne(context.TODO(), bson.D{{"title", "The Room"}}).Decode(&result)
	// end findOne
	if err != nil {
		if err == mongo.ErrNoDocuments {
			// This error means your query did not match any documents.
			return
		}
		panic(err)
	}
	// use Map() to quickly convert bson.D to a map
	output, err := json.MarshalIndent(result.Map(), "", "    ")
	if err != nil {
		panic(err)
	}
	fmt.Printf("%s\n", output)

{
    "imdb": [
        {
            "Key": "rating",
            "Value": 3.5
        },
        {
            "Key": "votes",
            "Value": 25673
        },
        {
            "Key": "id",
            "Value": 368226
        }
    ],
    "languages": [
        "English"
    ],
   ...
    "title": "The Room",
}

 



 Comments   
Comment by PM Bot [ 20/Sep/21 ]

There hasn't been any recent activity on this ticket, so we're resolving it. Thanks for reaching out! Please feel free to comment on this if you're able to provide more information.

Comment by Benji Rewis (Inactive) [ 03/Sep/21 ]

Would it be possible to use bson.MarshalExtJSON instead of json.MarshalIndent? The indents may not be there, but I believe those “Key” and “Value” printouts should be gone.

This is actually a very interesting little problem haha. All the solutions I’ve proposed above are slightly problematic. #3 might be API-breaking upon closer inspection. There is in fact existing behavior for calling json.Marshal on a bson.D. See code snippet below:

doc := bson.D{{"x", 1}, {"y", 2}}
docJson, err := json.Marshal(doc)
fmt.Println(string(docJson)) // prints [{"Key":"x","Value":1},{"Key":"y","Value":2}]

Implementing a custom primitive.D#MarshalJSON() may break users’ code that relies on the existing marshaling behavior.

However, #1 and #2 are more complex than they appear. The main issue is that it’s quite difficult to write a function that converts all embedded primitive.Ds to primitive.Ms for any generic container of a primitive.D (note that we could only recurse through bson.As, but that’s not really an exhaustive solution here). See the most recent comment on a draft implementation of primitive.D#RecursiveMap() that I put up.

#1 and #2 require some complex reflection logic to account for a generic, embedded slice containing bson.Ds. And, unfortunately, complex reflection logic can introduce bugs (even we find Go reflection to be quite opaque), so we’d like to save that as a last resort.

Let me know if MarshalExtJSON would work. We could even implement MarshalExtJSONIndent if that’s something that you’d find useful. If that doesn’t work for some reason, it might be time to reflect (pun intended).

Another alternative may be to implement the fmt.Stringer interface for primitive.D, but that’s a little more involved.

Comment by Nathan Leniz [ 31/Aug/21 ]

Nice summary. I agree that modifying primitive.D#Map to this behavior is the least desirable route, and adding an option to it isn't ideal either.

#3 seems like the best choice. Will we default to canonical extended JSON?

Comment by Benji Rewis (Inactive) [ 31/Aug/21 ]

Thanks again for the request nathan.leniz!

bson.D is just a type alias of primitive.D. So, the Map function that’s being calling in your example is primitive.D#Map(). As you mentioned, primitive.D#Map() is shallowly iterative: it will only make a new primitive.M (aliased by bson.M) with top-level keys and values copied from the original primitive.D. This shallow iteration means that any embedded arrays of documents (as in your example) or embedded documents will retain their original type. So, when you call json.MarshalIndent(…), those pesky “Key” and “Value” prepensions will remain on embedded bson.Ds or bson.As of bson.Ds.

Modifying primitive.D#Map() to be recursive is arguably an API-breaking change, as users may be relying on those “Key” and “Value” prepensions (even if they’re “buggy”). So, we have a couple options:

  1. Add a primitive.D#RecursiveMap() function that converts primitive.D to primitive.M recursively.
  2. Add a primitive.D#DebugString() function that creates a string similar to your desired JSON representation.
  3. Implement json.Marshaler for primitive.D so MarshalIndent() can be called directly on the primitive.D instance.
     

Any preference? I'm leaning toward #3.

Comment by Benji Rewis (Inactive) [ 30/Aug/21 ]

Thanks for the feature request nathan.leniz! Taking a look now.

Generated at Thu Feb 08 08:37:55 UTC 2024 using Jira 9.7.1#970001-sha1:2222b88b221c4928ef0de3161136cc90c8356a66.