Felder aus struct entfernen oder in JSON Response ausblenden

180

Ich habe in Go eine API erstellt, die beim Aufruf eine Abfrage ausführt, eine Instanz einer Struktur erstellt und diese Struktur dann als JSON codiert, bevor sie an den Aufrufer zurückgesendet wird. Ich möchte dem Aufrufer jetzt ermöglichen, die spezifischen Felder auszuwählen, die er zurückgeben möchte, indem er einen GET-Parameter "Felder" übergibt.

Dies bedeutet, dass sich meine Struktur abhängig von den Feldwerten ändern würde. Gibt es eine Möglichkeit, Felder aus einer Struktur zu entfernen? Oder sie zumindest dynamisch in der JSON-Antwort ausblenden? (Hinweis: Manchmal habe ich leere Werte, sodass das JSON-Tag omitEmpty hier nicht funktioniert.) Wenn beides nicht möglich ist, gibt es einen Vorschlag für eine bessere Vorgehensweise? Danke im Voraus.

Eine kleinere Version der von mir verwendeten Strukturen ist unten aufgeführt:

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

Ich codiere dann die Antwort und gebe sie wie folgt aus:

err := json.NewEncoder(c.ResponseWriter).Encode(&msg)
user387049
quelle
7
@Jacob, gemäß der aktualisierten Antwort von PuerkitoBio denke ich, dass Sie die Frage falsch gelesen haben. Die (derzeit) akzeptierte Antwort ist möglicherweise nicht die "richtige Antwort" auf Ihre Frage, sondern die hier gestellte! Die (derzeit) am höchsten bewertete Antwort kann Ihre Frage beantworten , ist jedoch auf diese nicht anwendbar !
Dave C

Antworten:

273

EDIT: Ich habe ein paar Abstimmungen bemerkt und mir diese Fragen und Antworten noch einmal angesehen. Die meisten Leute scheinen zu vermissen, dass das OP die dynamische Auswahl von Feldern basierend auf der vom Anrufer bereitgestellten Liste von Feldern verlangte . Sie können dies nicht mit dem statisch definierten json struct-Tag tun.

Wenn Sie immer ein Feld zur JSON-Codierung überspringen möchten json:"-", ignorieren Sie das Feld natürlich (beachten Sie auch, dass dies nicht erforderlich ist, wenn Ihr Feld nicht exportiert wird - diese Felder werden vom JSON-Encoder immer ignoriert). Das ist aber nicht die Frage des OP.

Um den Kommentar zur json:"-"Antwort zu zitieren :

Dies [die json:"-"Antwort] ist die Antwort, die die meisten Leute von der Suche hier haben möchten, aber es ist nicht die Antwort auf die Frage.


In diesem Fall würde ich anstelle einer Struktur eine map [string] -Schnittstelle {} verwenden. Sie können Felder einfach entfernen, indem Sie das deletein der Karte integrierte Element aufrufen, damit die Felder entfernt werden.

Das heißt, wenn Sie nicht erst die angeforderten Felder abfragen können.

mna
quelle
4
Sie möchten Ihre Typdefinition höchstwahrscheinlich nicht vollständig wegwerfen. Das wird auf der ganzen Linie lästig sein, beispielsweise wenn Sie andere Methoden für diesen Typ schreiben möchten, die auf diese Felder zugreifen. Die Verwendung eines Zwischenprodukts ist map[string]interface{}zwar sinnvoll, erfordert jedoch nicht, dass Sie Ihre Typdefinition wegwerfen.
Jorelli
1
Die andere Antwort ist die eigentliche Antwort auf diese Frage.
Jacob
1
Ein möglicher Nachteil des Löschens besteht darin, dass Sie manchmal mehrere JSON-Ansichten Ihrer Struktur (Map) unterstützen möchten. Beispiel: JSON-Ansicht für den Client ohne vertrauliches Feld und JSON-Ansicht für die Datenbank MIT dem vertraulichen Feld. Zum Glück ist es immer noch möglich, die Struktur zu verwenden - schauen Sie sich einfach meine Antwort an.
Adam Kurkiewicz
Dies funktioniert für mich, da ich nur eine bestimmte benötigte Id, aber nicht die gesamte json-Struktur zurückgeben möchte. Danke dafür!
Louie Miranda
154

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

GivenJazz
quelle
14
Ich würde @Jacob nicht zustimmen, weil das OP sagte, dass sie die Ausgabefelder basierend auf Abfragezeichenfolgeneinträgen in die API dynamisch steuern wollten. Wenn der Aufrufer der API beispielsweise nur nach Branche und Land fragt, müssen Sie den Rest entfernen. Aus diesem Grund wird die "angekreuzte" Antwort als Antwort auf diese Frage markiert. Diese hoch bewertete Antwort dient zum Markieren von Feldern, die für keinen eingebauten json-Marschall explizit verfügbar sind - EVER. Wenn Sie es dynamisch möchten, ist die angekreuzte Antwort die Antwort.
eduncan911
11
Dies ist die Antwort, die die meisten Leute von der Suche hier haben möchten, aber es ist nicht die Antwort auf die Frage.
Filip Haglund
5
Wie bereits erwähnt, forderte das OP eine Methode zur dynamischen Bildung eines DTO.
Codepushr
53

Ein anderer Weg , dies zu tun , ist es, eine Struktur zu haben , Zeiger mit dem ,omitemptyTag. Wenn die Zeiger Null sind , werden die Felder nicht gemarshallt.

Diese Methode erfordert keine zusätzliche Reflexion oder ineffiziente Verwendung von Karten.

Gleiches Beispiel wie Jorelli mit dieser Methode: http://play.golang.org/p/JJNa0m2_nw

Druska
quelle
3
+1 Stimme voll und ganz zu. Ich verwende diese Regel / diesen Trick die ganze Zeit mit den eingebauten Marshallern (und habe sogar einen CSV-Reader / -Schreiber basierend auf dieser Regel erstellt! - Ich kann das als weiteres Open-Source-Paket Open Source verwenden). Das OP konnte dann den Wert * Country einfach nicht auf Null setzen, und er würde weggelassen. Und großartig, dass Sie ein nettes, von y eingegebenes play.golang geliefert haben.
eduncan911
2
Natürlich erfordert diese Methode Reflexion, das JSON-zu-Struktur-Marshalling der stdlib verwendet immer Reflexion (tatsächlich verwendet es immer Reflexionsperiode, Karte oder Struktur oder was auch immer).
Mna
Ja, aber es erfordert keine zusätzliche Reflexion über Schnittstellen, was einige andere Antworten empfehlen.
Druska
14

Sie können das reflectPaket verwenden, um die gewünschten Felder auszuwählen, indem Sie über die Feld-Tags nachdenken und die jsonTag-Werte auswählen . Definieren Sie eine Methode auf dem Search Typ, der wählt die gewünschten Felder und gibt sie als ein map[string]interface{}, und dann Marschall , dass anstelle der Search selbst struct. Hier ist ein Beispiel, wie Sie diese Methode definieren können:

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
}

und hier ist eine ausführbare Lösung, die zeigt, wie Sie diese Methode aufrufen und Ihre Auswahl zusammenstellen würden: http://play.golang.org/p/1K9xjQRnO8

Jorelli
quelle
Wenn Sie sich das vorstellen, können Sie das Auswahlfeldmuster vernünftigerweise auf jeden Typ und jeden Tag-Schlüssel verallgemeinern. Es gibt nichts Spezielles für die SearchResult-Definition oder den JSON-Schlüssel.
Jorelli
Ich versuche, mich von Reflexionen fernzuhalten, aber dies speichert Typinformationen ziemlich gut ... Es ist schön, Code zu haben, der dokumentiert, wie Ihre Strukturen besser aussehen als eine Reihe von if / else-Tags in einer validate () -Methode (wenn Sie es überhaupt tun) haben eine)
Aktau
7

Ich habe gerade Sheriff veröffentlicht , der Strukturen in eine Karte umwandelt, die auf Tags basiert, die in den Strukturfeldern mit Anmerkungen versehen sind. Sie können dann die generierte Karte marshallen (JSON oder andere). Es erlaubt Ihnen wahrscheinlich nicht, nur die vom Anrufer angeforderten Felder zu serialisieren, aber ich kann mir vorstellen, dass die Verwendung einer Gruppe von Gruppen es Ihnen ermöglichen würde, die meisten Fälle abzudecken. Die direkte Verwendung von Gruppen anstelle der Felder würde höchstwahrscheinlich auch die Cache-Fähigkeit erhöhen.

Beispiel:

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

Nehmen Sie drei Zutaten:

  1. Das reflectPaket, das alle Felder einer Struktur durchläuft.

  2. Eine ifAnweisung zum Aufnehmen der gewünschten Felder Marshalund

  3. Das encoding/jsonPaket nach MarshalIhren Wünschen.

Vorbereitung:

  1. Mischen Sie sie in einem guten Verhältnis. Verwenden Sie reflect.TypeOf(your_struct).Field(i).Name()diese Option , um einen Namen für das idritte Feld von zu erhalten your_struct.

  2. Verwenden Sie reflect.ValueOf(your_struct).Field(i)diese Option , um eine Typdarstellung Valueeines ith-Felds von zu erhalten your_struct.

  3. Verwenden Sie fieldValue.Interface()diese Option, um den tatsächlichen Wert (der auf den Typ interface {} übertragen wurde) fieldValuedes Typs abzurufen Value(beachten Sie die Verwendung der Klammer - die Interface () -Methode erzeugtinterface{}

Wenn Sie es glücklicherweise schaffen, dabei keine Transistoren oder Leistungsschalter zu verbrennen, sollten Sie Folgendes erhalten:

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:

dienen mit einer beliebigen Struktur und einer Reihe map[string]boolvon Feldern, die Sie beispielsweise einschließen möchten

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

}

Guten Appetit!

Adam Kurkiewicz
quelle
Warnung! Wenn Ihre includeFields Feldnamen enthalten, die nicht mit den tatsächlichen Feldern übereinstimmen, erhalten Sie einen ungültigen JSON. Du wurdest gewarnt.
Adam Kurkiewicz
5

Sie können das Tagging-Attribut "omitifempty" verwenden oder optionale Feldzeiger erstellen und die zu überspringenden Felder nicht initialisiert lassen.

deemok
quelle
Dies ist die richtigste Antwort auf die Frage und den Anwendungsfall des OP.
user1943442
2
@ user1943442, nicht ist es nicht; Das OP erwähnt ausdrücklich , warum "Auslassung" nicht anwendbar ist.
Dave C
2

Ich war auch mit diesem Problem konfrontiert. Zuerst wollte ich nur die Antworten in meinem http-Handler spezialisieren. Mein erster Ansatz bestand darin, ein Paket zu erstellen, das die Informationen einer Struktur in eine andere Struktur kopiert und diese zweite Struktur dann zusammenstellt. Ich habe dieses Paket mit Reflection gemacht, also hat mir dieser Ansatz nie gefallen und ich war auch nicht dynamisch.

Deshalb habe ich beschlossen, das encoding / json-Paket zu ändern, um dies zu tun. Die Funktionen Marshal, MarshalIndentund (Encoder) Encodeempfängt zusätzlich ein

type F map[string]F

Ich wollte einen JSON der Felder simulieren, die zum Marshalling benötigt werden, daher werden nur die Felder in der Karte gemarshallt.

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
quelle
Ich habe es noch nicht ausprobiert, aber das sieht gut aus. Es wäre sogar noch besser, wenn auch die Marshaler-Schnittstelle unterstützt würde.
Huggie
1

Die Frage ist jetzt etwas alt, aber ich bin vor einiger Zeit auf dasselbe Problem gestoßen, und da ich keinen einfachen Weg gefunden habe, dies zu tun, habe ich eine Bibliothek gebaut, die diesen Zweck erfüllt. Es ermöglicht die einfache Generierung einer map[string]interface{}aus einer statischen Struktur.

https://github.com/tuvistavie/structomap

Daniel Perez
quelle
Sie können es jetzt ganz einfach mit einem Code-Snippet aus meinem Rezept tun.
Adam Kurkiewicz
Das Snippet ist eine Teilmenge der Bibliothek, aber ein Hauptproblem bei der Rückgabe von a []byteist, dass es nicht sehr wiederverwendbar ist: Es ist beispielsweise keine einfache Möglichkeit, anschließend ein Feld hinzuzufügen. Daher würde ich vorschlagen, einen zu erstellen map[string]interface{}und den JSON-Serialisierungsteil in die Standardbibliothek zu übernehmen.
Daniel Perez
1

Ich hatte nicht das gleiche Problem, aber ähnlich. Der folgende Code löst natürlich auch Ihr Problem, wenn Sie nichts gegen Leistungsprobleme haben. Bevor Sie diese Art von Lösung in Ihr System implementieren, empfehle ich Ihnen, Ihre Struktur neu zu gestalten, wenn Sie können. Das Senden einer Antwort mit variabler Struktur ist überentwickelt. Ich glaube, eine Antwortstruktur stellt einen Vertrag zwischen einer Anfrage und einer Ressource dar und sollte keine abhängigen Anfragen sein (Sie können unerwünschte Felder auf Null setzen, das tue ich). In einigen Fällen müssen wir dieses Design implementieren. Wenn Sie glauben, dass Sie sich in diesen Fällen befinden, finden Sie hier den von mir verwendeten Play-Link und Code.

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
quelle
1

Ich habe diese Funktion erstellt, um Struktur in JSON-Zeichenfolge zu konvertieren, indem einige Felder ignoriert wurden. Hoffe es wird helfen.

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
}

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

Chhaileng
quelle