Rimozione dei campi da struct o nasconderli in JSON Response


181

Ho creato un'API in Go che, una volta chiamato, esegue una query, crea un'istanza di una struttura e quindi codifica quella struttura come JSON prima di rispedirla al chiamante. Vorrei ora consentire al chiamante di poter selezionare i campi specifici che vorrebbero restituire passando un parametro GET "field".

Ciò significa che a seconda dei valori dei campi, la mia struttura cambierebbe. C'è un modo per rimuovere i campi da una struttura? O almeno nasconderli nella risposta JSON in modo dinamico? (Nota: a volte ho valori vuoti, quindi il tag omitEmpty JSON non funzionerà qui) Se nessuno di questi è possibile, c'è un suggerimento su un modo migliore per gestirlo? Grazie in anticipo.

Di seguito è riportata una versione più piccola delle strutture che sto usando:

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

Quindi codifico e produco la risposta in questo modo:

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

7
@Jacob, secondo la risposta aggiornata di PuerkitoBio, penso che tu abbia letto male la domanda. La (attualmente) accettata potrebbe non essere la "risposta corretta" alla tua domanda, ma è quella richiesta qui! La risposta (attualmente) più votata può rispondere alla tua domanda ma è completamente inapplicabile a questa!
Dave C,

Risposte:


275

EDIT: ho notato alcuni voti negativi e ho dato un'altra occhiata a queste domande e risposte. Alla maggior parte delle persone sembra che l'OP abbia richiesto la selezione dinamica dei campi in base all'elenco dei campi fornito dal chiamante. Non puoi farlo con il tag JSON Struct definito staticamente.

Se quello che vuoi è sempre saltare un campo per codificare json, ovviamente usa json:"-"per ignorare il campo (nota anche che questo non è necessario se il tuo campo non è esportato - quei campi sono sempre ignorati dal codificatore json). Ma questa non è la domanda del PO.

Per citare il commento sulla json:"-"risposta:

Questa [la json:"-"risposta] è la risposta che vorrebbe la maggior parte delle persone che finiscono qui per cercare, ma non è la risposta alla domanda.


Utilizzerei un'interfaccia map [string] {} invece di una struttura in questo caso. Puoi rimuovere facilmente i campi chiamando l' deleteintegrato sulla mappa per rimuovere i campi.

Cioè, se non è possibile eseguire query solo per i campi richiesti in primo luogo.


4
molto probabilmente non vuoi eliminare completamente la definizione del tuo tipo. Sarà fastidioso, come quando vuoi scrivere altri metodi su questo tipo che accedano a quei campi. L'uso di un intermedio map[string]interface{}ha senso, ma non richiede di eliminare la definizione del tipo.
jorelli,

1
L'altra risposta è la risposta effettiva a questa domanda.
Jacob,

1
Un possibile svantaggio dell'eliminazione è che a volte potresti voler supportare più viste json della tua struttura (mappa). Ad esempio la vista json per il client senza un campo sensibile e la vista json per il database CON il campo sensibile. Fortunatamente è ancora possibile usare la struttura - dai un'occhiata alla mia risposta.
Adam Kurkiewicz, l'

Questo funziona per me poiché avevo solo bisogno di uno specifico Id, ma non voglio restituire l'intera struttura JSON. Grazie per questo!
Louie Miranda,

155

usa `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


14
Non sarei d'accordo @Jacob perché l'OP ha affermato di voler controllare dinamicamente i campi di output in base alle voci della stringa di query nell'API. Ad esempio, se il chiamante all'API richiede solo Industria e Paese, sarà necessario rimuovere il resto. Questo è il motivo per cui la risposta "barrata" è contrassegnata come risposta a questa domanda. Questa risposta molto votata è per contrassegnare i campi esplicitamente mai disponibili per qualsiasi built-json-marshaler - MAI. se lo desideri in modo dinamico, la risposta selezionata è la risposta.
eduncan911,

11
Questa è la risposta che vorrebbe la maggior parte delle persone che finiscono qui per cercare, ma non è la risposta alla domanda.
Filip Haglund,

5
Come già affermato, il PO chiedeva un metodo per formare dinamicamente un DTO.
codepushr

53

Un altro modo per farlo è avere una struttura di puntatori con il ,omitemptytag. Se i puntatori sono nulli , i campi non verranno marshallizzati.

Questo metodo non richiederà ulteriori riflessioni o un uso inefficiente delle mappe.

Stesso esempio di jorelli che utilizza questo metodo: http://play.golang.org/p/JJNa0m2_nw


3
+1 Completamente d'accordo. Uso sempre questa regola / trucco con i marshaler integrati (e ho anche creato un lettore / scrittore CSV basato su questa regola! - Potrei open-source che non appena un altro pacchetto CSV go). L'OP non potrebbe quindi semplicemente impostare il valore * Paese su zero e verrebbe omesso. E fantastico che tu abbia fornito un bel gioco; hai anche scritto play.golang.
eduncan911,

2
Naturalmente quel metodo richiede riflessione, il marshalling json-to-struct dello stdlib usa sempre la riflessione (in realtà usa sempre il periodo di riflessione, la mappa o la struttura o altro).
mna,

Sì, ma non richiede ulteriori riflessioni utilizzando le interfacce, che alcune altre risposte raccomandano.
Druska,

14

È possibile utilizzare il reflectpacchetto per selezionare i campi desiderati riflettendo sui tag dei campi e selezionando i jsonvalori dei tag. Definire un metodo sui vostri SearchResults tipo che seleziona i campi desiderati e li restituisce come map[string]interface{}, e poi maresciallo che al posto delle SearchResults stessa struct. Ecco un esempio di come potresti definire quel metodo:

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
}

ed ecco una soluzione eseguibile che mostra come chiameresti questo metodo e esegui il marshalling della tua selezione: http://play.golang.org/p/1K9xjQRnO8


a pensarci bene, potresti ragionevolmente generalizzare il modello selectfields a qualsiasi tipo e qualsiasi chiave tag; non c'è nulla di specifico per la definizione SearchResult o la chiave json.
jorelli,

Sto cercando di stare lontano dalla riflessione, ma questo salva abbastanza bene le informazioni sul tipo ... È bello avere un codice che documenta l'aspetto delle tue strutture meglio di un gruppo di tag if / else in un metodo validate () (se anche uno)
Aktau,

7

Ho appena pubblicato lo sceriffo , che trasforma le strutture in una mappa basata su tag annotati sui campi della struttura. È quindi possibile eseguire il marshalling (JSON o altri) della mappa generata. Probabilmente non ti consente di serializzare solo l'insieme di campi richiesto dal chiamante, ma immagino che l'utilizzo di un insieme di gruppi ti consenta di coprire la maggior parte dei casi. L'uso diretto dei gruppi anziché dei campi aumenterebbe molto probabilmente anche la capacità della cache.

Esempio:

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

7

Prendi tre ingredienti:

  1. Il reflectpacchetto per scorrere su tutti i campi di una struttura.

  2. Una ifdichiarazione per raccogliere i campi che si desidera Marshale

  3. Il encoding/jsonpacchetto per Marshali campi di tuo gradimento.

Preparazione:

  1. Frullali in una buona proporzione. Utilizzare reflect.TypeOf(your_struct).Field(i).Name()per ottenere un nome del ith campo di your_struct.

  2. Utilizzare reflect.ValueOf(your_struct).Field(i)per ottenere una Valuerappresentazione di tipo di un ith campo di your_struct.

  3. Utilizzare fieldValue.Interface()per recuperare il valore effettivo (eseguito l'upgrade per digitare l'interfaccia {}) fieldValuedi tipo Value(notare l'uso della parentesi - il metodo Interface () produceinterface{}

Se per fortuna riesci a non bruciare transistor o interruttori nel processo, dovresti ottenere qualcosa del genere:

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
}

Servendo:

servire con una struttura arbitraria e una map[string]booldi campi che si desidera includere, ad esempio

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

}

Buon appetito!


Avvertimento! Se i tuoi includeField contengono nomi di campi che non corrispondono ai campi effettivi, otterrai un json non valido. Sei stato avvertito.
Adam Kurkiewicz, l'

5

È possibile utilizzare l'attributo di tag "omitifempty" o creare puntatori a campi opzionali e lasciare quelli che si desidera saltare non inizializzati.


Questa è la risposta più corretta alla domanda e al caso d'uso dei PO.
user1943442

2
@ user1943442, non lo è; il PO menziona esplicitamente perché "l'omogeneità" è inapplicabile.
Dave C

2

Ho anche affrontato questo problema, all'inizio volevo solo specializzare le risposte nel mio gestore http. Il mio primo approccio è stato la creazione di un pacchetto che copia le informazioni di una struttura in un'altra struttura e quindi esegue il marshalling di quella seconda struttura. Ho fatto quel pacchetto usando la riflessione, quindi non mi è mai piaciuto quell'approccio e non ero neanche dinamicamente.

Quindi ho deciso di modificare il pacchetto encoding / json per farlo. Le funzioni Marshal, MarshalIndente(Encoder) Encode inoltre riceve un

type F map[string]F

Volevo simulare un JSON dei campi necessari per eseguire il marshalling, quindi esegue il marshalling solo dei campi presenti nella mappa.

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

Non l'ho ancora provato, ma sembra fantastico. Sarebbe ancora meglio se anche l'interfaccia Marshaler fosse supportata.
Huggie,

1

La domanda ora è un po 'vecchia, ma mi sono imbattuto nello stesso problema un po' di tempo fa e, poiché non ho trovato un modo semplice per farlo, ho costruito una biblioteca che soddisfaceva questo scopo. Permette di generare facilmente un map[string]interface{}da una struttura statica.

https://github.com/tuvistavie/structomap


Ora puoi farlo facilmente usando uno snippet di codice della mia ricetta.
Adam Kurkiewicz, l'

Lo snippet è un sottoinsieme della libreria, ma qui un grosso problema sulla restituzione di a []byteè che non è molto riutilizzabile: ad esempio un modo semplice per aggiungere un campo in seguito. Quindi suggerirei di creare un map[string]interface{}e lasciare la parte di serializzazione JSON nella libreria standard.
Daniel Perez,

1

Non ho avuto lo stesso problema ma simile. Il codice seguente risolve anche il tuo problema, ovviamente se non ti dispiace il problema delle prestazioni. Prima di implementare quel tipo di soluzione sul tuo sistema, ti consiglio di riprogettare la tua struttura, se puoi. L'invio della risposta della struttura variabile è eccessivo. Credo che una struttura di risposta rappresenti un contratto tra una richiesta e una risorsa e non dovrebbe dipendere da richieste (è possibile rendere nulli i campi non desiderati). In alcuni casi dobbiamo implementare questo progetto, se ritieni di esserlo in questi casi ecco il link di riproduzione e il codice che uso.

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

Ho creato questa funzione per convertire struct in stringa JSON ignorando alcuni campi. Spero che possa essere d'aiuto.

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
}

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.