I have a struct Table with 2 Players, but I need to ignore some properties from the struct Player
when I send JSON.
I could use json:"-"
, but then the property will be ignored ALWAYS, and I need to ignore it only when I send the Table struct. I need those properties when I send the Player
in other parts of the code.
I have:
type Player struct {
Id Int64 `json:"id"`
Username string `json:"username,omitempty"`
Password string `json:"-,omitempty"`
Email string `json:"email,omitempty"`
Birthdate time.Time `json:"birthdate,omitempty"`
Avatar string `json:avatar,omitempty"`
}
type Table struct {
Id int `json:"id"`
PlayerTop Player `json:"playerTop"`
PlayerBottom Player `json:"playerBottom"`
}
I need:
{
"Table": {
"id": 1,
"playerBottom": {
"id": 1,
"username": "peter",
"avatar": "avatar.png"
},
"playerTop": {
"id": 1,
"username": "peter",
"avatar": "avatar.png"
}
}
}
The players come from the database, so the properties aren't empty.
a) I could do something like:
myTable = new(Table)
myTable.PlayerBottom.Email = ""
myTable.PlayerBottom.Birthdate = ""
myTable.PlayerTop.Email = ""
myTable.PlayerTop.Birthdate = ""
so those properties will be ignored in the JSON, thanks to json:"omitempty"
, but this is a bad idea.
b) I could use something like an alias struct but Table
is expecting that PlayerBottom
is of type Player
not PlayerAlias
, but I don't know how to implement it:
type PlayerAlias struct {
Id Int64 `json:"id"`
Username string `json:"username,omitempty"`
Avatar string `json:avatar,omitempty"`
}
c) I tried to add dynamically json:"-"
to the properties that I don't want from the JSON before to send it, but it was a mess.
You could create a custom Marshaler
for Table
types. This is the interface you have to implement:
https://golang.org/pkg/encoding/json/#Marshaler
type Marshaler interface {
MarshalJSON() ([]byte, error)
}
Then you'd remove the -
tag from Player
(because when you marshal it elsewhere you need to preserve the fields) and only ignore it in the custom MarshalJSON
method of Table
.
Here's a simple (unrelated) example of implementing custom marshaling for a type, encoding one of the fields in hex:
type Account struct {
Id int32
Name string
}
func (a Account) MarshalJSON() ([]byte, error) {
m := map[string]string{
"id": fmt.Sprintf("0x%08x", a.Id),
"name": a.Name,
}
return json.Marshal(m)
}
func main() {
joe := Account{Id: 123, Name: "Joe"}
fmt.Println(joe)
s, _ := json.Marshal(joe)
fmt.Println(string(s))
}
As you can see here, such marshaling is easy to do by constructing a map
with just the fields you need and passing it to json.Marshal
. For your Table
and Player
this will result in just a few lines of trivial code. IMHO it's better to do this than to modify the types and complicate them with embeddings/aliases, just for the sake of JSON encoding.
There's a couple of ways you can achieve this. The first would be to create a custom marshaller for the Table
type. This is, however somewhat tedious, and can be quite restrictive. There is, IMHO, an easier way to do the same thing: embed types:
type PartialPlayer struct {
Player // embed the entire type
Email string `json:"-"` // override fields and add the tag to exclude them
Birthdate string `json:"-"`
}
Now you can still access all data you want, and you could even add getters for indirect data access:
func (pp PartialPlayer) GetEmail() string {
if pp.Email == "" {
return pp.Player.Email // get embedded Email value
}
return pp.Email // add override value
}
Note that you don't need to use these getter functions. The Id
field is not overridden, so if I have a PartialPlayer
variable, I can access the value directly:
pp := PartialPlayer{
Player: playerVar,
}
fmt.Printf("Player ID: %v
", pp.Id) // still works
You can access overridden/masked fields by specifying you want the value held on the embedded type, without a function, too:
fmt.Printf("Email on partial: '%s', but I can see '%s'
", pp.Email, pp.Player.Email)
The latter will print Email on partial: '', but I can see 'foo@bar.com'
.
Use this type in Table
like this:
type Table struct {
Id int `json:"id"`
PlayerTop PartialPlayer `json:"playerTop"`
PlayerBottom PartialPlayer `json:"playerBottom"`
}
Initialise:
tbl := Table{
Id: 213,
PlayerTop: PartialPlayer{
Player: playerVar,
},
PlayerBottom: PartialPlayer{
Player: player2Var,
},
}
That works just fine. The benefit of this approach is that marshalling to and from JSON doesn't require a call to your custom marshaller functions, and creating/mapping intermediary types like maps or hidden types etc...
Should you want to hade another field, just add it to the PartialPlayer
type. Should you want to unhide a field like Email
, just remove it from the PartialPlayer
type, job done.
Now for an approach with a custom marshaller:
type Table struct {
Id int `json:"id"`
PlayerTop Player `json:"playerTop"`
PlayerBottom Player `json:"playerBottom"`
}
type marshalTable {
Table
// assuming the PartialPlayer type above
PlayerTop PartialPlayer `json:"playerTop"`
PlayerBottom PartialPlayer `json:"playerBottom"`
}
func (t Table) MarshalJSON() ([]byte, error) {
mt := marshalTable{
Table: t,
PlayerTop: PartialPlayer{
Player: t.PlayerTop,
},
PlayerBottom: PartialPlayer{
Player: t.PlayerBottom,
},
}
return json.Marshal(mt)
}
It's not too different from building a type map[string]interface{}
here, but by using type embedding, you don't have to update the marshaller function every time a field is renamed or changed on the Player
type.
Using this approach, your Table
type can be used in the exact same way as you're doing right now, but the JSON output will not include the Email
and Birthdate
fields.
Types that only differ in their field tags are convertible to one another since Go 1.8. So you can define one or more "view" types for players and pick one that fits your use case when marshaling.
The advantage over embedding or implementing json.Marshaler is that every time you add a new field to Player
the compiler forces you to update every view type as well, i.e. you have to make a conscious decision whether or not to include the new field in each view.
package main
import (
"encoding/json"
"fmt"
"time"
)
type Player struct {
Id int64 `json:"id"`
Username string `json:"username,omitempty"`
Password string `json:"-,omitempty"`
Email string `json:"email,omitempty"`
Birthdate time.Time `json:"birthdate,omitempty"`
Avatar string `json:"avatar,omitempty"`
}
// PlayerSummary has the same underlying type as Player, but omits some fields
// in the JSON representation.
type PlayerSummary struct {
Id int64 `json:"id"`
Username string `json:"username,omitempty"`
Password string `json:"-"`
Email string `json:"-"`
Birthdate time.Time `json:"-"`
Avatar string `json:"avatar,omitempty"`
}
type Table struct {
Id int `json:"id"`
PlayerTop PlayerSummary `json:"playerTop"`
PlayerBottom PlayerSummary `json:"playerBottom"`
}
func main() {
p1 := Player{
Id: 1,
Username: "Alice",
Email: "alice@example.com",
Birthdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
Avatar: "https://www.gravatar.com/avatar/c160f8cc69a4f0bf2b0362752353d060",
}
p2 := Player{
Id: 2,
Username: "Bob",
Email: "bob@example.com",
Birthdate: time.Date(1998, 6, 1, 0, 0, 0, 0, time.UTC),
Avatar: "https://www.gravatar.com/avatar/4b9bb80620f03eb3719e0a061c14283d",
}
b, _ := json.MarshalIndent(Table{
Id: 0,
PlayerTop: PlayerSummary(p1), // marshal p1 as PlayerSummary
PlayerBottom: PlayerSummary(p2), // marshal p2 as PlayerSummary
}, "", " ")
fmt.Println(string(b))
}
// Output:
// {
// "id": 0,
// "playerTop": {
// "id": 1,
// "username": "Alice",
// "avatar": "https://www.gravatar.com/avatar/c160f8cc69a4f0bf2b0362752353d060"
// },
// "playerBottom": {
// "id": 2,
// "username": "Bob",
// "avatar": "https://www.gravatar.com/avatar/4b9bb80620f03eb3719e0a061c14283d"
// }
// }
Try it on the Playground: https://play.golang.org/p/a9V2uvOJX3Y
Aside: Consider removing the Password field from Player. The password (hash) is typically only used by very few functions. Functions that do need it can accept the player and password as separate arguments. That way you eliminate the risk of accidentally leaking the password (in log messages, for instance).
The custom marshaller is a great way to change how your object is mapped to JSON. In you case however, I would not suggest this, in case you ever need to map your entire object to JSON at some other point (i.e. for an admin tool).
Some key points of this answer:
I would suggest simply defining a function on your struct the returns a map of the fields you wish to expose.
From your example:
type Player struct {
Id int64 `json:"id"`
Username string `json:"username,omitempty"`
Password string `json:"-,omitempty"`
Email string `json:"email,omitempty"`
Birthdate time.Time `json:"birthdate,omitempty"`
Avatar string `json:"avatar,omitempty"`
}
func (p Player) PublicInfo() map[string]interface{} {
return map[string]interface{}{
"id": p.Id,
"username": p.Username,
"avatar": p.Avatar,
}
}
There are several ways you can bubble the use of this function up. One simple way is to have the Table
struct use maps for PlayerTop
and PlayerBottom
:
type Table struct {
Id int `json:"id"`
PlayerTop map[string]interface{} `json:"playerTop"`
PlayerBottom map[string]interface{} `json:"playerBottom"`
}
func NewTable(id int, playerTop, playerBottom Player) Table {
return Table{Id: id,
PlayerTop: playerTop.PublicInfo(),
PlayerBottom: playerBottom.PublicInfo()}
}
Marshalling this to JSON will return the fields you want. And you only need to edit one place to add/remove fields from the JSON.
In case you use the Table
type internally and need to access the players from it, then you may still need to store the full Player
struct on the Table
. I would simply follow the Public
pattern from above with table like so:
type Table struct {
Id int `json:"id"`
PlayerTop Player `json:"playerTop"`
PlayerBottom Player `json:"playerBottom"`
}
func (t Table) PublicInfo() map[string]interface{} {
return map[string]interface{}{
"id": t.Id,
"playerTop": t.PlayerTop.PublicInfo(),
"playerBottom": t.PlayerBottom.PublicInfo(),
}
}
Now when you create a table and use it internally its clear what the types are, and when you marshall the JSON it's clear that you are excluding some types and where that exclusion is taking place.
func main() {
p1 := Player{Id: 1, Username: "peter", Avatar: "avatar.png", Email: "PRIVATE"}
p2 := Player{Id: 1, Username: "peter", Avatar: "avatar.png", Email: "PRIVATE"}
t := Table{Id: 1, PlayerTop: p1, PlayerBottom: p2}
admin, _ := json.Marshal(t)
public, _ := json.Marshal(t.PublicInfo())
fmt.Println(fmt.Sprintf("For admins: %s", string(admin)))
fmt.Println(fmt.Sprintf("For public: %s", string(public)))
}
/*
Output:
For admins: {"id":1,"playerTop":{"id":1,"username":"peter","email":"PRIVATE","birthdate":"0001-01-01T00:00:00Z","avatar":"avatar.png"},"playerBottom":{"id":1,"username":"peter","email":"PRIVATE","birthdate":"0001-01-01T00:00:00Z","avatar":"avatar.png"}}
For public: {"id":1,"playerBottom":{"avatar":"avatar.png","id":1,"username":"peter"},"playerTop":{"avatar":"avatar.png","id":1,"username":"peter"}}
*/
See it in action: https://play.golang.org/p/24t-B6ZuUKu
If you want to represent a public and private version of data - and one version is a superset of the other, try embedded structs. Adding a custom JSON marshaller and you can get two presentations of the same core data.
Database JSON: {"Id":12345,"PlayerTop":{"id":456,"username":"Peter","avatar":"peter.png","password":"Secr3t","birthdate":"0001-01-01T00:00:00Z"},"PlayerBottom":{"id":890,"username":"Paul","avatar":"paul.png","password":"abc123","birthdate":"0001-01-01T00:00:00Z"}}
Public JSON: {"id":12345,"playerTop":{"id":456,"username":"Peter","avatar":"peter.png"},"playerBottom":{"id":890,"username":"Paul","avatar":"paul.png"}}
Run in playground:
// public info
type PublicPlayer struct {
Id int64 `json:"id"`
Username string `json:"username,omitempty"`
Avatar string `json:"avatar,omitempty"`
}
// private info
type Player struct {
PublicPlayer // embed public info
Password string `json:"password,omitempty"`
Email string `json:"email,omitempty"`
Birthdate time.Time `json:"birthdate,omitempty"`
}
type Table struct {
Id int `json:"id"`
PlayerTop Player `json:"playerTop"`
PlayerBottom Player `json:"playerBottom"`
}
// derivative type, so we can add a custom marshaller
type PublicTable Table
func (t PublicTable) MarshalJSON() ([]byte, error) {
return json.Marshal(
// anonymous struct definition
struct {
Id int `json:"id"`
Top PublicPlayer `json:"playerTop"`
Bottom PublicPlayer `json:"playerBottom"`
}{
t.Id,
t.PlayerTop.PublicPlayer, // only export public data
t.PlayerBottom.PublicPlayer, // only export public data
},
)
}