web-dev-qa-db-fra.com

Supprimer des champs de la structure ou les masquer dans une réponse JSON

J'ai créé dans Go une API qui, lorsqu'elle est appelée, effectue une requête, crée une instance d'une structure, puis l'encode sous forme de code JSON avant de la renvoyer à l'appelant. J'aimerais maintenant permettre à l'appelant de pouvoir sélectionner les champs spécifiques qu'il souhaite voir renvoyés en transmettant un paramètre GET "fields".

Cela signifie que selon la valeur des champs, ma structure changerait. Est-il possible de supprimer des champs d'une structure? Ou du moins les cacher dans la réponse JSON de manière dynamique? (Remarque: j'ai parfois des valeurs vides pour que la balise JSON omitEmpty ne fonctionne pas ici). Si aucune de ces options n'est possible, existe-t-il une suggestion sur une meilleure façon de gérer cela? Merci d'avance.

Une version plus petite des structures que j'utilise est ci-dessous:

type SearchResult struct {
    Date        string      `json:"date"`
    IdCompany   int         `json:"idCompany"`
    Company     string      `json:"company"`
    IdIndustry  interface{} `json:"idIndustry"`
    Industry    string      `json:"industry"`
    IdContinent interface{} `json:"idContinent"`
    Continent   string      `json:"continent"`
    IdCountry   interface{} `json:"idCountry"`
    Country     string      `json:"country"`
    IdState     interface{} `json:"idState"`
    State       string      `json:"state"`
    IdCity      interface{} `json:"idCity"`
    City        string      `json:"city"`
} //SearchResult

type SearchResults struct {
    NumberResults int            `json:"numberResults"`
    Results       []SearchResult `json:"results"`
} //type SearchResults

J'ai ensuite encodé et sorti la réponse comme suit:

err := json.NewEncoder(c.ResponseWriter).Encode(&msg)
146
user387049

EDIT: J'ai remarqué quelques votes négatifs et j’ai jeté un nouveau regard sur ce Q & A. La plupart des gens semblent oublier que le PO a demandé que les champs soient dynamiquement sélectionnés en fonction de la liste de champs fournie par l'appelant. Vous ne pouvez pas faire cela avec la balise json struct définie statiquement.

Si ce que vous voulez, c'est toujours ignorer un champ à coder json, utilisez bien sûr json:"-" pour ignorer le champ (notez également que c'est pas obligatoire si votre champ n’est pas exporté - ces champs sont toujours ignorés par le codeur json). Mais ce n'est pas la question du PO.

Pour citer le commentaire sur la réponse json:"-":

Cette [la json:"-" réponse] est la réponse que voudraient la plupart des gens qui se retrouvent ici après une recherche, mais ce n'est pas la réponse à la question.


J'utiliserais une interface [string] interface {} au lieu d'une structure dans ce cas. Vous pouvez facilement supprimer des champs en appelant le delete intégré à la carte pour supprimer les champs.

Autrement dit, si vous ne pouvez pas interroger uniquement les champs demandés.

222
mna

utilisez `json:" - "`

// Field is ignored by this package.
Field int `json:"-"`

// Field appears in JSON as key "myName".
Field int `json:"myName"`

// Field appears in JSON as key "myName" and
// the field is omitted from the object if its value is empty,
// as defined above.
Field int `json:"myName,omitempty"`

// Field appears in JSON as key "Field" (the default), but
// the field is skipped if empty.
// Note the leading comma.
Field int `json:",omitempty"`

doc: http://golang.org/pkg/encoding/json/#Marshal

141
GivenJazz

Une autre façon de faire est d’avoir une structure de pointeurs avec la balise ,omitempty. Si les pointeurs sont nil , les champs ne seront pas Marshallés.

Cette méthode ne nécessitera pas de réflexion supplémentaire ou une utilisation inefficace des cartes.

Même exemple que jorelli avec cette méthode: http://play.golang.org/p/JJNa0m2_nw

46
Druska

Vous pouvez utiliser le package reflect pour sélectionner les champs souhaités en réfléchissant sur les étiquettes de champ et en sélectionnant les valeurs de l'étiquette json. Définissez une méthode sur votre type SearchResults qui sélectionne les champs souhaités et les renvoie sous forme de map[string]interface{}, puis indiquez que à la place du paramètre SearchResults. struct lui-même. Voici un exemple de la façon dont vous pourriez définir cette méthode:

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
}

et voici une solution exécutable qui montre comment vous appelez cette méthode et commentez votre sélection: http://play.golang.org/p/1K9xjQRnO8

12
jorelli

Je viens de publier shérif , qui transforme les structures en une carte basée sur les balises annotées sur les champs de structure. Vous pouvez ensuite marshaler (JSON ou autres) la carte générée. Cela ne vous permet probablement pas de ne sérialiser que l'ensemble des champs demandés par l'appelant, mais j'imagine que l'utilisation d'un ensemble de groupes vous permettrait de couvrir la plupart des cas. L'utilisation de groupes au lieu de champs directement augmenterait probablement la capacité de cache.

Exemple:

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:    "[email protected]",
        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)
}
5
Michael Weibel

Prenez trois ingrédients:

  1. Le paquetage reflect à boucler sur tous les champs d'une structure.

  2. Une instruction if pour sélectionner les champs que vous souhaitez Marshal, et

  3. Le package encoding/json vers Marshal les champs de votre choix.

Préparation:

  1. Mélangez-les dans une bonne proportion. Utilisez reflect.TypeOf(your_struct).Field(i).Name() pour obtenir le nom du champ ith de your_struct.

  2. Utilisez reflect.ValueOf(your_struct).Field(i) pour obtenir une représentation de type Value d'un ith champ de your_struct.

  3. Utilisez fieldValue.Interface() pour récupérer la valeur réelle (transposée en amont dans l'interface de type {}) de la fieldValue de type Value (notez le crochet utilisé - l'interface () méthode = produit interface{}

Si vous réussissez heureusement à ne pas brûler de transistors ou de disjoncteurs, vous devriez obtenir quelque chose comme ceci:

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
}

Portion:

sert avec une structure arbitraire et un map[string]bool des champs que vous voulez inclure, par exemple

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 appétit!

5
Adam Kurkiewicz

Vous pouvez utiliser l'attribut de marquage "omitifempty" ou créer des pointeurs de champs optionnels et laisser les champs souhaités non initialisés.

3
deemok

La question est maintenant un peu ancienne, mais j’ai rencontré le même problème il ya quelque temps et, comme je n’ai trouvé aucun moyen facile de le faire, j’ai construit une bibliothèque répondant à cet objectif. Cela permet de générer facilement un map[string]interface{} à partir d'une structure statique.

https://github.com/tuvistavie/structomap

1
Daniel Perez

Je n'ai pas eu le même problème mais similaire. Le code ci-dessous résout également votre problème, bien sûr si cela ne vous gêne pas. Avant de mettre en place ce type de solution sur votre système, je vous recommande de revoir votre structure si vous le pouvez. L'envoi d'une réponse à structure variable est une ingénierie excessive. Je crois qu'une structure de réponse représente un contrat entre une demande et une ressource et ne devrait pas être une demande de dépendance (vous pouvez rendre les champs non souhaités nuls, comme je le fais). Dans certains cas, nous devons implémenter cette conception, si vous pensez que vous êtes dans ce cas, voici le lien de lecture et le code que j'utilise.

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

}
1
RockOnGom

J'ai également fait face à ce problème, au début je voulais juste spécialiser les réponses dans mon gestionnaire http. Ma première approche consistait à créer un paquet qui copiait les informations d'une structure vers une autre structure, puis organisait cette seconde structure. J'ai fait ce paquet en utilisant la réflexion, donc, jamais aimé cette approche et je n'étais pas dynamique non plus.

J'ai donc décidé de modifier le paquet encoding/json pour ce faire. Les fonctions Marshal, MarshalIndent et (Encoder) Encode reçoivent en outre un

type F map[string]F

Je voulais simuler un JSON des champs nécessaires au marshal, de sorte qu'il ne regroupe que les champs figurant sur la carte.

https://github.com/JuanTorr/jsont

package main

import (
    "fmt"
    "log"
    "net/http"

    "github.com/JuanTorr/jsont"
)

type SearchResult struct {
    Date        string      `json:"date"`
    IdCompany   int         `json:"idCompany"`
    Company     string      `json:"company"`
    IdIndustry  interface{} `json:"idIndustry"`
    Industry    string      `json:"industry"`
    IdContinent interface{} `json:"idContinent"`
    Continent   string      `json:"continent"`
    IdCountry   interface{} `json:"idCountry"`
    Country     string      `json:"country"`
    IdState     interface{} `json:"idState"`
    State       string      `json:"state"`
    IdCity      interface{} `json:"idCity"`
    City        string      `json:"city"`
} //SearchResult

type SearchResults struct {
    NumberResults int            `json:"numberResults"`
    Results       []SearchResult `json:"results"`
} //type SearchResults
func main() {
    msg := SearchResults{
        NumberResults: 2,
        Results: []SearchResult{
            {
                Date:        "12-12-12",
                IdCompany:   1,
                Company:     "alfa",
                IdIndustry:  1,
                Industry:    "IT",
                IdContinent: 1,
                Continent:   "america",
                IdCountry:   1,
                Country:     "México",
                IdState:     1,
                State:       "CDMX",
                IdCity:      1,
                City:        "Atz",
            },
            {
                Date:        "12-12-12",
                IdCompany:   2,
                Company:     "beta",
                IdIndustry:  1,
                Industry:    "IT",
                IdContinent: 1,
                Continent:   "america",
                IdCountry:   2,
                Country:     "USA",
                IdState:     2,
                State:       "TX",
                IdCity:      2,
                City:        "XYZ",
            },
        },
    }
    fmt.Println(msg)
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {

        //{"numberResults":2,"results":[{"date":"12-12-12","idCompany":1,"idIndustry":1,"country":"México"},{"date":"12-12-12","idCompany":2,"idIndustry":1,"country":"USA"}]}
        err := jsont.NewEncoder(w).Encode(msg, jsont.F{
            "numberResults": nil,
            "results": jsont.F{
                "date":       nil,
                "idCompany":  nil,
                "idIndustry": nil,
                "country":    nil,
            },
        })
        if err != nil {
            log.Fatal(err)
        }
    })

    http.ListenAndServe(":3009", nil)
}
0
Juan Torres