Removing fields from struct or hiding them in JSON Response

后端 未结 13 1345
孤街浪徒
孤街浪徒 2020-12-12 09:37

I\'ve created an API in Go that, upon being called, performs a query, creates an instance of a struct, and then encodes that struct as JSON before sending back to the caller

相关标签:
13条回答
  • 2020-12-12 10:22

    I didn't have the same problem but similar. Below code solves your problem too, of course if you don't mind performance issue. Before implement that kind of solution to your system I recommend you to redesign your structure if you can. Sending variable structure response is over-engineering. I believe a response structure represents a contract between a request and resource and it should't be depend requests.(you can make un-wanted fields null, I do). In some cases we have to implement this design, if you believe you are in that cases here is the play link and code I use.

    type User2 struct {
        ID       int    `groups:"id" json:"id,omitempty"`
        Username string `groups:"username" json:"username,omitempty"`
        Nickname string `groups:"nickname" json:"nickname,omitempty"`
    }
    
    type User struct {
        ID       int    `groups:"private,public" json:"id,omitempty"`
        Username string `groups:"private" json:"username,omitempty"`
        Nickname string `groups:"public" json:"nickname,omitempty"`
    }
    
    var (
        tagName = "groups"
    )
    
    //OmitFields sets fields nil by checking their tag group value and access control tags(acTags)
    func OmitFields(obj interface{}, acTags []string) {
        //nilV := reflect.Value{}
        sv := reflect.ValueOf(obj).Elem()
        st := sv.Type()
        if sv.Kind() == reflect.Struct {
            for i := 0; i < st.NumField(); i++ {
                fieldVal := sv.Field(i)
                if fieldVal.CanSet() {
                    tagStr := st.Field(i).Tag.Get(tagName)
                    if len(tagStr) == 0 {
                        continue
                    }
                    tagList := strings.Split(strings.Replace(tagStr, " ", "", -1), ",")
                    //fmt.Println(tagList)
                    // ContainsCommonItem checks whether there is at least one common item in arrays
                    if !ContainsCommonItem(tagList, acTags) {
                        fieldVal.Set(reflect.Zero(fieldVal.Type()))
                    }
                }
            }
        }
    }
    
    //ContainsCommonItem checks if arrays have at least one equal item
    func ContainsCommonItem(arr1 []string, arr2 []string) bool {
        for i := 0; i < len(arr1); i++ {
            for j := 0; j < len(arr2); j++ {
                if arr1[i] == arr2[j] {
                    return true
                }
            }
        }
        return false
    }
    func main() {
        u := User{ID: 1, Username: "very secret", Nickname: "hinzir"}
        //assume authenticated user doesn't has permission to access private fields
        OmitFields(&u, []string{"public"}) 
        bytes, _ := json.Marshal(&u)
        fmt.Println(string(bytes))
    
    
        u2 := User2{ID: 1, Username: "very secret", Nickname: "hinzir"}
        //you want to filter fields by field names
        OmitFields(&u2, []string{"id", "nickname"}) 
        bytes, _ = json.Marshal(&u2)
        fmt.Println(string(bytes))
    
    }
    
    0 讨论(0)
  • 2020-12-12 10:23

    I just published sheriff, which transforms structs to a map based on tags annotated on the struct fields. You can then marshal (JSON or others) the generated map. It probably doesn't allow you to only serialize the set of fields the caller requested, but I imagine using a set of groups would allow you to cover most cases. Using groups instead of the fields directly would most likely also increase cache-ability.

    Example:

    package main
    
    import (
        "encoding/json"
        "fmt"
        "log"
    
        "github.com/hashicorp/go-version"
        "github.com/liip/sheriff"
    )
    
    type User struct {
        Username string   `json:"username" groups:"api"`
        Email    string   `json:"email" groups:"personal"`
        Name     string   `json:"name" groups:"api"`
        Roles    []string `json:"roles" groups:"api" since:"2"`
    }
    
    func main() {
        user := User{
            Username: "alice",
            Email:    "alice@example.org",
            Name:     "Alice",
            Roles:    []string{"user", "admin"},
        }
    
        v2, err := version.NewVersion("2.0.0")
        if err != nil {
            log.Panic(err)
        }
    
        o := &sheriff.Options{
            Groups:     []string{"api"},
            ApiVersion: v2,
        }
    
        data, err := sheriff.Marshal(o, user)
        if err != nil {
            log.Panic(err)
        }
    
        output, err := json.MarshalIndent(data, "", "  ")
        if err != nil {
            log.Panic(err)
        }
        fmt.Printf("%s", output)
    }
    
    0 讨论(0)
  • 2020-12-12 10:24

    Take three ingredients:

    1. The reflect package to loop over all the fields of a struct.

    2. An if statement to pick up the fields you want to Marshal, and

    3. The encoding/json package to Marshal the fields of your liking.

    Preparation:

    1. Blend them in a good proportion. Use reflect.TypeOf(your_struct).Field(i).Name() to get a name of the ith field of your_struct.

    2. Use reflect.ValueOf(your_struct).Field(i) to get a type Value representation of an ith field of your_struct.

    3. Use fieldValue.Interface() to retrieve the actual value (upcasted to type interface{}) of the fieldValue of type Value (note the bracket use - the Interface() method produces interface{}

    If you luckily manage not to burn any transistors or circuit-breakers in the process you should get something like this:

    func MarshalOnlyFields(structa interface{},
        includeFields map[string]bool) (jsona []byte, status error) {
        value := reflect.ValueOf(structa)
        typa := reflect.TypeOf(structa)
        size := value.NumField()
        jsona = append(jsona, '{')
        for i := 0; i < size; i++ {
            structValue := value.Field(i)
            var fieldName string = typa.Field(i).Name
            if marshalledField, marshalStatus := json.Marshal((structValue).Interface()); marshalStatus != nil {
                return []byte{}, marshalStatus
            } else {
                if includeFields[fieldName] {
                    jsona = append(jsona, '"')
                    jsona = append(jsona, []byte(fieldName)...)
                    jsona = append(jsona, '"')
                    jsona = append(jsona, ':')
                    jsona = append(jsona, (marshalledField)...)
                    if i+1 != len(includeFields) {
                        jsona = append(jsona, ',')
                    }
                }
            }
        }
        jsona = append(jsona, '}')
        return
    }
    

    Serving:

    serve with an arbitrary struct and a map[string]bool of fields you want to include, for example

    type magic struct {
        Magic1 int
        Magic2 string
        Magic3 [2]int
    }
    
    func main() {
        var magic = magic{0, "tusia", [2]int{0, 1}}
        if json, status := MarshalOnlyFields(magic, map[string]bool{"Magic1": true}); status != nil {
            println("error")
        } else {
            fmt.Println(string(json))
        }
    
    }
    

    Bon Appetit!

    0 讨论(0)
  • 2020-12-12 10:26

    You can use the reflect package to select the fields that you want by reflecting on the field tags and selecting the json tag values. Define a method on your SearchResults type that selects the fields you want and returns them as a map[string]interface{}, and then marshal that instead of the SearchResults struct itself. Here's an example of how you might define that method:

    func fieldSet(fields ...string) map[string]bool {
        set := make(map[string]bool, len(fields))
        for _, s := range fields {
            set[s] = true
        }
        return set
    }
    
    func (s *SearchResult) SelectFields(fields ...string) map[string]interface{} {
        fs := fieldSet(fields...)
        rt, rv := reflect.TypeOf(*s), reflect.ValueOf(*s)
        out := make(map[string]interface{}, rt.NumField())
        for i := 0; i < rt.NumField(); i++ {
            field := rt.Field(i)
            jsonKey := field.Tag.Get("json")
            if fs[jsonKey] {
                out[jsonKey] = rv.Field(i).Interface()
            }
        }
        return out
    }
    

    and here's a runnable solution that shows how you would call this method and marshal your selection: http://play.golang.org/p/1K9xjQRnO8

    0 讨论(0)
  • 2020-12-12 10:27

    Here is how I defined my structure.

    type User struct {
        Username string  `json:"username" bson:"username"`
        Email    string  `json:"email" bson:"email"`
        Password *string `json:"password,omitempty" bson:"password"`
        FullName string  `json:"fullname" bson:"fullname"`
    }
    

    And inside my function set user.Password = nil for not to be Marshalled.

    0 讨论(0)
  • 2020-12-12 10:31

    The question is now a bit old, but I came across the same issue a little while ago, and as I found no easy way to do this, I built a library fulfilling this purpose. It allows to easily generate a map[string]interface{} from a static struct.

    https://github.com/tuvistavie/structomap

    0 讨论(0)
提交回复
热议问题