隐藏JSON中的属性

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:

  • All values are exposed internally
  • From the marshall code it's clear that values will be excluded and it's easy to get to the code that excludes values
  • Minimize repetition and new types

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
                },  
        )   
}