`json.Marshal()` with additional fields in Go
I have a Go struct that I want to encode to JSON with some additional fields.
The struct looks like this:
type Actor struct {
Context []Context `bson:"@context,omitempty" json:"@context,omitempty"`
Kind Kind `bson:"type,omitempty" json:"type,omitempty"`
ID string `bson:"id,omitempty" json:"id,omitempty"`
PreferredUsername string `bson:"preferredUsername,omitempty" json:"preferredUsername,omitempty"`
Name string `bson:"name,omitempty" json:"name,omitempty"`
Summary string `bson:"summary,omitempty" json:"summary,omitempty"`
Icon []string `bson:"icon,omitempty" json:"icon,omitempty"`
PublicKey *PublicKey `bson:"-" json:"publicKey,omitempty"`
url *url.URL
}
I want to add additional "following"
, "followers"
, "liked"
, "inbox"
, and "outbox"
fields to the encoded JSON. These are strings obtained as follows:
func (a *Actor) Following() string {
url := a.getURL()
url.Path = path.Join(url.Path, EndpointFollowing)
return url.String()
}
func (a *Actor) Followers() string {
url := a.getURL()
url.Path = path.Join(url.Path, EndpointFollowers)
return url.String()
}
func (a *Actor) Liked() string {
url := a.getURL()
url.Path = path.Join(url.Path, EndpointLiked)
return url.String()
}
func (a *Actor) Inbox() string {
url := a.getURL()
url.Path = path.Join(url.Path, EndpointInbox)
return url.String()
}
func (a *Actor) Outbox() string {
url := a.getURL()
url.Path = path.Join(url.Path, EndpointOutbox)
return url.String()
}
I ended up doing this by overriding MarshalJSON()
and creating a new struct with an anonymous Actor
field plus the additional fields:
func (a *Actor) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Actor
Following string `json:"following"`
Followers string `json:"followers"`
Liked string `json:"liked"`
Inbox string `json:"inbox"`
Outbox string `json:"outbox"`
}{
Actor: *a,
Following: a.Following(),
Followers: a.Followers(),
Liked: a.Liked(),
Inbox: a.Inbox(),
Outbox: a.Outbox(),
})
}
As a result, this instance of Actor
:
actor = &Actor{
Context: []Context{CtxActivityStreams},
Kind: KindPerson,
ID: "https://social.example/alyssa",
PreferredUsername: "alyssa",
}
Gets encoded into this when calling json.Marshal(actor)
:
{"@context": ["https://www.w3.org/ns/activitystreams"],
"type": "Person",
"id": "https://social.example/alyssa",
"preferredUsername": "alyssa",
"inbox": "https://social.example/alyssa/inbox.json",
"outbox": "https://social.example/alyssa/outbox.json",
"followers": "https://social.example/alyssa/followers.json",
"following": "https://social.example/alyssa/following.json",
"liked": "https://social.example/alyssa/liked.json"}
Which can be tested like this (only showing one test case for the purpose of this post):
func TestActorMarshal(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
preferredUsername string
wantJSON string
}{
{
name: "success",
preferredUsername: "alyssa",
wantJSON: fmt.Sprintf(`{"@context": ["https://www.w3.org/ns/activitystreams"],
"type": "%s",
"id": "https://social.example/alyssa",
"preferredUsername": "alyssa",
"inbox": "https://social.example/alyssa/inbox.json",
"outbox": "https://social.example/alyssa/outbox.json",
"followers": "https://social.example/alyssa/followers.json",
"following": "https://social.example/alyssa/following.json",
"liked": "https://social.example/alyssa/liked.json"}`, KindPerson),
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
actor, err := NewActor(KindPerson, "https://social.example/", "alyssa")
require.NoError(t, err)
gotJSON, err := json.Marshal(actor)
require.NoError(t, err)
require.JSONEq(t, tc.wantJSON, string(gotJSON))
})
}
}