根据用户角色控制字段的可见性

I'd like to hide/show some fields of a model depending on User role.

What would be the most idiomatic way to implement it?

I don't really want to create N different types of the same model (where N is amount of User roles). Like: UserEmployee, AdminEmployee, WhateverEmployee.

It would be perfect if there is some solution that uses the tags for it:

type Employee struct {
   ID string `visibility:"admin,user"`
   Name string `visibility:"admin,user"`
   Salary int `visibility:"admin"`
}

jsonBytes, _ := someLib.Marshal(Employee{"1", "John", 5000}, "user")

fmt.Println(string(jsonBytes)) // {"id":"1","name":"John"}

The question is really pretty broad. I just wanted to know how you handle this situation or what is the most common way to do it in the Go community. I want clean and centralized (same for all models) solution that won't require to produce tons of duplicated code.

What have I tried before: I've just tried to use separate models for all cases and cast between them.

  1. Create an empty struct of your type (Employee in this problem) that will hold the filtered data.
  2. Use the reflect package to compare if the field tag contains the desired tag value (visibility role).
  3. Copy values of base struct to our filter struct when we find a tag match and json marshal the output struct:

package main

import (
    "encoding/json"
    "fmt"
    "reflect"
    "strings"
)

type Employee struct {
    ID       string          `visibility:"admin, hr, user" json:"id,omitempty"`
    Name     string          `visibility:"admin, hr, user" json:"name,omitempty"`
    Salary   int             `visibility:"admin, hr" json:"salary,omitempty"`
    Password string          `visibility:"admin" json:"password,omitempty"`
    Rights   map[string]bool `visibility:"admin" json:"rights,omitempty"`
    Boss     *Employee       `visibility:"admin, hr" json:"boss,omitempty"`
}

func filterEmployee(emp Employee, role string) Employee {
    var fEmployee Employee
    ev := reflect.ValueOf(emp)
    et := reflect.TypeOf(emp)

    // Iterate through each field within the struct
    for i := 0; i < ev.NumField(); i++ {
        v := ev.Field(i)
        t := et.Field(i)
        roles := t.Tag.Get("visibility")

        if strings.Contains(roles, role) {
            switch i {
            case 0: // ID
                fEmployee.ID = v.String()
            case 1: // Name
                fEmployee.Name = v.String()
            case 2: // Salary
                fEmployee.Salary = int(v.Int())
            case 3: // Password
                fEmployee.Password = v.String()
            case 4: // Rights
                fEmployee.Rights = v.Interface().(map[string]bool)
            case 5: // Boss
                fEmployee.Boss = v.Interface().(*Employee)
            }
        }
    }
    return fEmployee
}

func main() {

    e := Employee{
        "1",
        "Jack",
        100000,
        "password321",
        map[string]bool{"create": false, "update": false},
        &Employee{
            "2",
            "John",
            120000,
            "pwd",
            map[string]bool{"create": true, "update": true},
            nil,
        },
    }

    fuser := filterEmployee(e, "user")
    fhr := filterEmployee(e, "hr")
    fadmin := filterEmployee(e, "admin")

    buser, err := json.MarshalIndent(fuser, "", "  ")
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println("Filtering with role user: ")
    fmt.Println(string(buser))

    bhr, err := json.MarshalIndent(fhr, "", "  ")
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println("
Filtering with role hr: ")
    fmt.Println(string(bhr))

    badmin, err := json.MarshalIndent(fadmin, "", "  ")
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println("
Filtering with role admin: ")
    fmt.Println(string(badmin))
}

Output:

Filtering with role user: 
{
  "id": "1",
  "name": "Jack"
}

Filtering with role hr: 
{
  "id": "1",
  "name": "Jack",
  "salary": 100000,
  "boss": {
    "id": "2",
    "name": "John",
    "salary": 120000,
    "password": "pwd",
    "rights": {
      "create": true,
      "update": true
    }
  }
}

Filtering with role admin: 
{
  "id": "1",
  "name": "Jack",
  "salary": 100000,
  "password": "password321",
  "rights": {
    "create": false,
    "update": false
  },
  "boss": {
    "id": "2",
    "name": "John",
    "salary": 120000,
    "password": "pwd",
    "rights": {
      "create": true,
      "update": true
    }
  }
}

Playground

EDIT: Updated answer for asker's request.

View the old playground for previous answer that ran into issues.

Old Playground

Use "omit empty"

type Employee struct {
    ID string `json:",omitempty"`
    Name string `json:",omitempty"`
    Salary int `json:",omitempty"`
}

Your function can look like

func MarshallEmployee(e Employee, permission string) {
    if permission == "user"{
        e.Salary = 0
    }
    ....marshall it
}

or you could also just not add the value to the struct in the first place. See the docs for more detail.