Supprimer des champs de struct ou les masquer dans JSON Response

181

J'ai créé une API dans Go qui, une fois appelée, exécute une requête, crée une instance d'une structure, puis encode cette structure en JSON avant de la renvoyer à l'appelant. Je voudrais maintenant permettre à l'appelant d'être en mesure de sélectionner les champs spécifiques qu'ils aimeraient retourner en passant un paramètre GET "champs".

Cela signifie qu'en fonction de la ou des valeurs des champs, ma structure changerait. Existe-t-il un moyen de supprimer des champs d'une structure? Ou du moins les cacher dans la réponse JSON de manière dynamique? (Remarque: parfois, j'ai des valeurs vides, donc la balise JSON omitEmpty ne fonctionnera pas ici) Si aucun de ces éléments 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'encode et sort ensuite la réponse comme ceci:

err := json.NewEncoder(c.ResponseWriter).Encode(&msg)
user387049
la source
7
@Jacob, selon la réponse mise à jour de PuerkitoBio, je pense que vous avez mal lu la question. La (actuellement) acceptée n'est peut-être pas la "bonne réponse" à votre question, mais c'est celle qui est posée ici! La réponse (actuellement) la plus votée peut répondre à votre question mais est totalement inapplicable à celle-ci!
Dave C

Réponses:

275

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

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

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

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


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

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

mna
la source
4
vous ne voudrez probablement pas jeter complètement votre définition de type. Cela va être gênant sur toute la ligne, comme lorsque vous voulez écrire d'autres méthodes sur ce type qui accèdent à ces champs. L'utilisation d'un intermédiaire a du map[string]interface{}sens, mais cela ne nécessite pas que vous jetiez votre définition de type.
jorelli
1
L'autre réponse est la réponse réelle à cette question.
Jacob
1
Un inconvénient possible de la suppression est que vous souhaiterez parfois prendre en charge plusieurs vues json de votre structure (map). Par exemple, vue json pour le client sans champ sensible et vue json pour la base de données AVEC le champ sensible. Heureusement, il est toujours possible d'utiliser la structure - jetez un œil à ma réponse.
Adam Kurkiewicz
Cela fonctionne pour moi car je n'avais besoin que d'un spécifique Idmais, je ne veux pas retourner la structure json entière. Merci pour cela!
Louie Miranda
155

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

DonnéJazz
la source
14
Je ne suis pas d'accord avec @Jacob parce que l'OP a déclaré qu'il souhaitait contrôler dynamiquement les champs de sortie en fonction des entrées de chaîne de requête de l'API. Par exemple, si l'appelant à l'API ne demande que le secteur et le pays, vous devrez alors supprimer le reste. C'est pourquoi la réponse «cochée» est marquée comme réponse à cette question. Cette réponse hautement votée est pour marquer les champs explicitement jamais-disponibles-pour-aucun-construit-json-marshaler - JAMAIS. si vous le voulez dynamiquement, la réponse cochée est la réponse.
eduncan911
11
C'est la réponse que la plupart des gens qui se retrouvent ici après une recherche voudraient, mais ce n'est pas la réponse à la question.
Filip Haglund
5
Comme déjà indiqué, le PO demandait une méthode pour former dynamiquement un DTO.
codepushr
53

Une autre façon de faire est d'avoir une structure de pointeurs avec la ,omitemptybalise. Si les pointeurs sont nuls , 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 utilisant cette méthode: http://play.golang.org/p/JJNa0m2_nw

Druska
la source
3
+1 Tout à fait d'accord. J'utilise cette règle / astuce tout le temps avec les marshalers intégrés (et j'ai même construit un lecteur / écrivain CSV basé sur cette règle également! - Je peux ouvrir le code source dès qu'un autre package csv go). L'OP pourrait alors simplement ne pas définir la valeur * Country sur nil, et elle serait omise. Et génial que vous ayez fourni un joli; y tapé play.golang aussi.
eduncan911
2
Bien sûr, cette méthode nécessite une réflexion, le marshaling json-to-struct du stdlib utilise toujours la réflexion (en fait, il utilise toujours une période de réflexion, une map ou une structure ou autre).
mna
Oui, mais cela ne nécessite pas de réflexion supplémentaire en utilisant des interfaces, ce que d'autres réponses recommandent.
Druska
14

Vous pouvez utiliser le reflectpackage pour sélectionner les champs souhaités en réfléchissant sur les balises de champ et en sélectionnant les jsonvaleurs de balise. Définissez une méthode sur votre type SearchResults qui sélectionne les champs souhaités et les renvoie en tant que map[string]interface{}, puis marshalez cela au lieu de la structure SearchResults elle-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 appelleriez cette méthode et organiseriez votre sélection: http://play.golang.org/p/1K9xjQRnO8

Jorelli
la source
en y réfléchissant, vous pouvez raisonnablement généraliser le modèle selectfields à n'importe quel type et n'importe quelle clé de balise; il n'y a rien à ce sujet qui soit spécifique à la définition SearchResult ou à la clé json.
jorelli
J'essaie de rester à l'écart de la réflexion, mais cela économise assez bien les informations de type ... C'est bien d'avoir du code qui documente mieux à quoi vos structures ressemblent qu'un tas de balises if / else dans une méthode validate () (si vous avoir un)
Aktau
7

Je viens de publier sheriff , qui transforme les structures en une carte basée sur des 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 sérialiser uniquement 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 directe de groupes au lieu des champs augmenterait probablement également la capacité du 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)
}
Michael Weibel
la source
7

Prenez trois ingrédients:

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

  2. Une ifdéclaration pour sélectionner les champs que vous souhaitez Marshal, et

  3. Le encoding/jsonforfait aux Marshaldomaines de votre goût.

Préparation:

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

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

  3. Utilisez fieldValue.Interface()pour récupérer la valeur réelle (remontée vers le type interface {}) fieldValuedu type de Value(notez l'utilisation des crochets - la méthode Interface () produitinterface{}

Si vous parvenez heureusement à ne pas brûler de transistors ou de disjoncteurs dans le processus, 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:

servir avec une structure arbitraire et un map[string]booldes champs que vous souhaitez 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!

Adam Kurkiewicz
la source
Avertissement! Si vos includeFields contiennent des noms de champs qui ne correspondent pas aux champs réels, vous obtiendrez un json non valide. Tu étais prévenu.
Adam Kurkiewicz
5

Vous pouvez utiliser l'attribut de balisage "omitifempty" ou créer des champs optionnels pointeurs et laisser ceux que vous voulez ignorer non initialisés.

juger
la source
C'est la réponse la plus correcte à la question et au cas d'utilisation des PO.
user1943442
2
@ user1943442, non ce n'est pas; le PO mentionne explicitement pourquoi "omitempty" est inapplicable.
Dave C
2

J'ai également rencontré 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 package qui copie les informations d'une structure dans une autre structure, puis à marshaler cette seconde structure. J'ai fait ce package en utilisant la réflexion, donc, je n'ai jamais aimé cette approche et je n'étais pas non plus dynamique.

J'ai donc décidé de modifier le package encoding / json pour ce faire. Les fonctions Marshal, MarshalIndentet (Encoder) Encodereçoit en outre un

type F map[string]F

Je voulais simuler un JSON des champs nécessaires pour marshaler, donc il ne rassemble que les champs qui sont dans 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)
}
Juan Torres
la source
Je ne l'ai pas encore essayé, mais ça a l'air génial. Ce serait encore mieux si l'interface Marshaler est également prise en charge.
huggie le
1

La question est maintenant un peu ancienne, mais je suis tombé sur le même problème il y a quelque temps, et comme je ne trouvais pas de moyen facile de le faire, j'ai construit une bibliothèque remplissant cet objectif. Il permet de générer facilement un à map[string]interface{}partir d'une structure statique.

https://github.com/tuvistavie/structomap

Daniel Perez
la source
Vous pouvez maintenant le faire facilement en utilisant un extrait de code de ma recette.
Adam Kurkiewicz
L'extrait de code est un sous-ensemble de la bibliothèque, mais un problème majeur ici à propos du retour de a []byteest qu'il n'est pas très réutilisable: pas de moyen facile d'ajouter un champ par la suite, par exemple. Je suggérerais donc de créer un map[string]interface{}et de laisser la partie de sérialisation JSON dans la bibliothèque standard.
Daniel Perez
1

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 le problème de performances ne vous dérange pas. Avant d'implémenter ce type de solution dans votre système, je vous recommande de repenser votre structure si vous le pouvez. Envoyer une réponse de structure variable est une sur-ingénierie. Je crois qu'une structure de réponse représente un contrat entre une demande et une ressource et qu'elle ne devrait pas dépendre des demandes (vous pouvez rendre les champs indésirables nuls, 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))

}
RockOnGom
la source
1

J'ai créé cette fonction pour convertir une structure en chaîne JSON en ignorant certains champs. J'espère que cela aidera.

func GetJSONString(obj interface{}, ignoreFields ...string) (string, error) {
    toJson, err := json.Marshal(obj)
    if err != nil {
        return "", err
    }

    if len(ignoreFields) == 0 {
        return string(toJson), nil
    }

    toMap := map[string]interface{}{}
    json.Unmarshal([]byte(string(toJson)), &toMap)

    for _, field := range ignoreFields {
        delete(toMap, field)
    }

    toJson, err = json.Marshal(toMap)
    if err != nil {
        return "", err
    }
    return string(toJson), nil
}

Exemple: https://play.golang.org/p/nmq7MFF47Gp

Chhaileng
la source