I'm building a RESTful API with Go and MongoDB, and I'm running into some difficulty with embedding the JSON for one document inside the JSON for another. Here's a toy example of what I'm trying to accomplish. I have the following schemas:
type Post struct {
ID bson.ObjectId `json:"id,omitempty"`
Title string `json:"title,omitempty"`
Owner bson.ObjectId `json:"owner,omitempty"` // references a User
}
type User struct {
ID bson.ObjectId `json:"id,omitempty"`
Name string `json:"name,omitempty"`
}
When creating the JSON for a post, I'd like to first look up the owner of the post in MongoDB and embed the resulting user inside said post's JSON (in-place of the original ObjectId
), like so:
{
"id": "...",
"title": "My awesome post",
"owner": {
"id": "...",
"name": "Cody"
}
}
I'm not quite sure how to accomplish this, other than manually constructing the JSON using map[string]interface{}
, like so:
post := LookupPost(...)
user := LookupUser(post.Owner)
m := map[string]interface{}{
"id": post.ID,
"title": post.Title,
"owner": map[string]interface{}{
"id": user.ID,
"name": user.Name,
},
}
b, _ := json.Marshal(m)
Obviously this doesn't scale very well isn't very DRY -- ideally, I'd be able to utilize the json
tags in each struct definition and have the fields inserted automatically.
Am I missing something, or is what I'm trying to do impossible? Or am I simply not approaching MongoDB/JSON in Go correctly? To put things in perspective, I'm coming from a Node.js background, where this sort of functionality is trivial.
Edit
To clarify things, here's some incorrect Go code that shows what I'd like to do
func getPostJSON() []byte {
p := LookupPost(...)
u := LookupUser(p.Owner, ...)
uj, _ := json.Marshal(u)
p.Owner = uj // can't do this in Go
pj, _ := json.Marshal(p)
return pj
}
So I actually discovered a much cleaner solution to this problem:
type Post struct {
ID bson.ObjectId `bson:"_id,omitempty" json:"id,omitempty"`
Title string `bson:"title,omitempty" json:"title,omitempty"`
Owner UserRef `bson:"owner,omitempty" json:"owner,omitempty"`
}
type User struct {
ID bson.ObjectId `json:"id,omitempty"`
Name string `json:"name,omitempty"`
}
type UserRef bson.ObjectId
func (ref UserRef) GetBSON() (interface{}, error) {
return bson.ObjectId(ref), nil
}
func (ref UserRef) MarshalJSON() ([]byte, error) {
u := LookupUserInMongoDB(ref)
return json.Marshal(u)
}
Here's how it works -- mgo can't store UserRef as an ObjectId when converting a Post to bson, so we can implement the GetBSON
method for UserRef to return the underlying ObjectId. This allows us to store Owner as an ObjectId in the database. And, like in @DaveC's answer, we implement the MarshalJSON
method for UserRef so that when converting a Post to json, we can replace the ObjectId with an actual embedded user.
I'm not familar with MongoDB or bson.ObjectId
, but can you substitute your own type for your User
field and have MongoDB easily fill that in for you from a user's bson.ObjectId
?
If so you can just wrap user object id's into their own type that implements the json.Marshaler
interface. E.g.:
// Embedded (instead of `type x bson.ObjectId`) so that we
// get all the methods and satisfy all the interfaces that
// bson.ObjectId does. Hopefully that's engough to allow MongoDB
// to fill in fields of this type from a database??
type ownerObjID struct{ bson.ObjectId }
// Here we marshal the results of looking up the user from the id
// rather than just the ID itself.
func (oid ownerObjID) MarshalJSON() ([]byte, error) {
user, err := LookupUser(oid.ObjectId)
if err != nil {
return nil, err
}
return json.Marshal(user)
}
type Post struct {
ID bson.ObjectId `json:"id,omitempty"`
Title string `json:"title,omitempty"`
Owner ownerObjID `json:"owner,omitempty"` // <-- is this type wrapping doable/easy with MongoDB?
}
type User struct {
ID bson.ObjectId `json:"id,omitempty"`
Name string `json:"name,omitempty"`
}
func main() {
post := LookupPost()
b, err := json.MarshalIndent(post, "", " ")
if err != nil {
log.Fatal(err)
}
fmt.Printf("JSON:
%s
", b)
}
// Some stubs for demo:
func LookupPost() Post {
return Post{
ID: "postID001",
Title: "Ima Test",
Owner: ownerObjID{"ownerID002"},
}
}
func LookupUser(id bson.ObjectId) (User, error) {
return User{
ID: id,
Name: "name for " + string(id),
}, nil
}
Gives me:
JSON:
{
"id": "postID001",
"title": "Ima Test",
"owner": {
"id": "ownerID002",
"name": "name for ownerID002"
}
}