Usuwanie pól ze struktury lub ukrywanie ich w odpowiedzi JSON

181

Stworzyłem API w Go, które po wywołaniu wykonuje zapytanie, tworzy instancję struktury, a następnie koduje tę strukturę jako JSON przed wysłaniem z powrotem do dzwoniącego. Chciałbym teraz pozwolić wywołującemu na wybranie określonych pól, które chcieliby zwrócić, przekazując parametr GET „pola”.

Oznacza to, że w zależności od wartości pól moja struktura uległaby zmianie. Czy istnieje sposób na usunięcie pól ze struktury? Lub przynajmniej dynamicznie ukryć je w odpowiedzi JSON? (Uwaga: czasami mam puste wartości, więc tag JSON omitEmpty nie będzie działał w tym miejscu). Jeśli żadne z powyższych nie jest możliwe, czy istnieje sugestia dotycząca lepszego sposobu rozwiązania tego problemu? Z góry dziękuję.

Poniżej znajduje się mniejsza wersja struktur, których używam:

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

Następnie koduję i wyprowadzam odpowiedź w następujący sposób:

err := json.NewEncoder(c.ResponseWriter).Encode(&msg)
user387049
źródło
7
@Jacob, zgodnie z zaktualizowaną odpowiedzią PuerkitoBio, myślę, że źle przeczytałeś pytanie. (Obecnie) zaakceptowana może nie być „poprawną odpowiedzią” na Twoje pytanie, ale jest odpowiedzią na tę zadaną tutaj! (Obecnie) najwyżej oddana odpowiedź może odpowiedzieć na Twoje pytanie, ale nie ma ona zastosowania w przypadku tego!
Dave C

Odpowiedzi:

275

EDYCJA: Zauważyłem kilka głosów przeciw i ponownie przyjrzałem się temu pytaniu i odpowiedziom. Większość ludzi wydaje się nie zauważyć, że program operacyjny poprosił o dynamiczne wybieranie pól na podstawie listy pól dostarczonej przez dzwoniącego. Nie możesz tego zrobić za pomocą statycznie zdefiniowanego tagu struct json.

Jeśli chcesz zawsze pomijać pole do kodowania json, użyj oczywiście json:"-"do zignorowania pola (pamiętaj również, że nie jest to wymagane, jeśli twoje pole nie jest eksportowane - te pola są zawsze ignorowane przez koder json). Ale to nie jest pytanie PO.

Cytując komentarz do json:"-"odpowiedzi:

To [ json:"-"odpowiedź] jest odpowiedzią, jakiej chciałaby większość ludzi kończących tutaj wyszukiwanie, ale nie jest to odpowiedź na pytanie.


W tym przypadku użyłbym map [string] interface {} zamiast struktury. Możesz łatwo usunąć pola, wywołując deletewbudowane na mapie pola do usunięcia.

Oznacza to, że jeśli nie możesz zapytać tylko o żądane pola w pierwszej kolejności.

mna
źródło
4
najprawdopodobniej nie chcesz całkowicie odrzucać swojej definicji typu. Będzie to kłopotliwe w przyszłości, na przykład gdy chcesz napisać inne metody tego typu, które mają dostęp do tych pól. Używanie półproduktu map[string]interface{}ma sens, ale nie wymaga odrzucania definicji typu.
jorelli
1
Druga odpowiedź to właściwa odpowiedź na to pytanie.
Jacob,
1
Możliwą wadą usuwania jest to, że czasami możesz chcieć obsługiwać wiele widoków json swojej struktury (mapy). Na przykład widok json dla klienta bez wrażliwego pola i widok json dla bazy danych z wrażliwym polem. Na szczęście nadal można korzystać ze struktury - wystarczy spojrzeć na moją odpowiedź.
Adam Kurkiewicz
Działa to dla mnie, ponieważ potrzebowałem tylko określonej, Idale nie chcę zwracać całej struktury json. Dzięki za to!
Louie Miranda
155

użyj `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
źródło
14
Nie zgodziłbym się z @Jacob, ponieważ OP powiedział, że chce dynamicznie kontrolować pola wyjściowe na podstawie wpisów ciągu zapytania do API. Na przykład, jeśli wywołujący interfejs API pyta tylko o branżę i kraj, musisz usunąć resztę. Dlatego „zaznaczona” odpowiedź jest oznaczona jako odpowiedź na to pytanie. Ta wysoko oceniana odpowiedź służy do oznaczania pól jawnie nigdy nie dostępnych dla żadnego wbudowanego-json-marshaler - EVER. jeśli chcesz, aby było to dynamicznie, odpowiedzią jest zaznaczona odpowiedź.
eduncan911
11
To jest odpowiedź, której chciałaby większość ludzi kończących tutaj wyszukiwania, ale nie jest to odpowiedź na pytanie.
Filip Haglund
5
Jak już wspomniano, PO prosił o metodę dynamicznego tworzenia DTO.
codepushr
53

Innym sposobem na zrobienie tego jest utworzenie struktury wskaźników ze ,omitemptyznacznikiem. Jeśli wskaźniki są zerowe , pola nie zostaną uporządkowane.

Ta metoda nie będzie wymagała dodatkowej refleksji ani nieefektywnego wykorzystania map.

Ten sam przykład co jorelli przy użyciu tej metody: http://play.golang.org/p/JJNa0m2_nw

Druska
źródło
3
+1 Całkowicie się zgadzam. Używam tej reguły / triku przez cały czas z wbudowanymi organizatorami (a nawet zbudowałem czytnik / program do zapisywania CSV również oparty na tej zasadzie! - mogę otworzyć kod źródłowy, gdy tylko kolejny pakiet csv go). PO mógłby wtedy po prostu nie ustawić wartości * Kraj na zero i zostałaby pominięta. I niesamowite, że dostarczyłeś miły; y wpisany również play.golang.
eduncan911
2
Oczywiście ta metoda wymaga refleksji, kierowanie json-to-struct w standardowym standardzie zawsze używa refleksji (w rzeczywistości zawsze używa okresu odbicia, mapy, struktury lub cokolwiek innego).
mna
Tak, ale nie wymaga dodatkowej refleksji z wykorzystaniem interfejsów, co zalecają inne odpowiedzi.
Druska
14

Możesz użyć reflectpakietu, aby wybrać żądane pola, odzwierciedlając znaczniki pól i wybierając jsonwartości znaczników. Zdefiniować metodę na swoim searchresults typ, który wybiera pola i zwraca je w map[string]interface{}, a następnie zebrać że zamiast się SearchResults struct się. Oto przykład, jak możesz zdefiniować tę metodę:

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
}

a oto gotowe do uruchomienia rozwiązanie, które pokazuje, jak wywołać tę metodę i zorganizować wybór: http://play.golang.org/p/1K9xjQRnO8

jorelli
źródło
jeśli się nad tym zastanowić, możesz rozsądnie uogólnić wzorzec selectfields na dowolny typ i dowolny klucz znacznika; nie ma w tym nic, co jest specyficzne dla definicji SearchResult lub klucza json.
jorelli
Próbuję trzymać się z daleka od refleksji, ale to całkiem ładnie zapisuje informacje o typie ... Fajnie jest mieć kod, który dokumentuje wygląd twoich struktur lepiej niż kilka tagów if / else w metodzie validate () (jeśli nawet mieć jeden)
Aktau
7

Właśnie opublikowałem szeryfa , który przekształca struktury w mapę na podstawie tagów z adnotacjami w polach struktury. Następnie możesz zorganizować (JSON lub inne) wygenerowaną mapę. Prawdopodobnie nie pozwala ci tylko na serializację zestawu pól, o które prosił dzwoniący, ale wyobrażam sobie, że użycie zestawu grup pozwoliłoby ci objąć większość przypadków. Użycie grup zamiast pól bezpośrednio prawdopodobnie również zwiększyłoby pojemność pamięci podręcznej.

Przykład:

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
źródło
7

Weź trzy składniki:

  1. reflectPakiet do pętli na wszystkich polach struct.

  2. ifOświadczenie odebrać pola, które chcesz Marshal, a

  3. encoding/jsonPakiet Marshaldziedzinach własnych upodobań.

Przygotowanie:

  1. Wymieszaj je w dobrej proporcji. Użyj, reflect.TypeOf(your_struct).Field(i).Name()aby uzyskać nazwę itego pola your_struct.

  2. Służy reflect.ValueOf(your_struct).Field(i)do uzyskiwania Valuereprezentacji typu dla ith pola your_struct.

  3. Służy fieldValue.Interface()do pobierania rzeczywistej wartości (przesłanej do typu interfejs {}) fieldValuetypu Value(zwróć uwagę na użycie nawiasu - metoda Interface () dajeinterface{}

Jeśli na szczęście uda ci się nie spalić żadnych tranzystorów lub wyłączników w procesie, powinieneś dostać coś takiego:

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
}

Porcja:

na przykład służyć z dowolną strukturą i map[string]boolpolami, które chcesz uwzględnić

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

}

Smacznego!

Adam Kurkiewicz
źródło
Ostrzeżenie! Jeśli twoje includeFields zawierają nazwy pól, które nie są zgodne z rzeczywistymi polami, otrzymasz nieprawidłowy plik json. Zostałeś ostrzeżony.
Adam Kurkiewicz
5

Możesz użyć atrybutu tagowania „omitifempty” lub utworzyć wskaźniki do pól opcjonalnych i pozostawić te, które chcesz pominąć, niezainicjalizowane.

deemok
źródło
Jest to najbardziej poprawna odpowiedź na pytanie dotyczące PO i przypadek użycia.
user1943442
2
@ user1943442, nie tak nie jest; OP wyraźnie wspomina, dlaczego „pomiń pustą” nie ma zastosowania.
Dave C
2

Też napotkałem ten problem, na początku chciałem tylko wyspecjalizować odpowiedzi w moim module obsługi http. Moje pierwsze podejście polegało na utworzeniu pakietu, który kopiuje informacje o strukturze do innej struktury, a następnie organizuje tę drugą strukturę. Zrobiłem ten pakiet używając refleksji, więc nigdy nie podobało mi się to podejście, a także nie byłem dynamiczny.

Postanowiłem więc zmodyfikować pakiet encoding / json, aby to zrobić. Funkcje Marshal, MarshalIndenta (Encoder) Encodedodatkowo otrzymuje plik

type F map[string]F

Chciałem zasymulować JSON pól, które są potrzebne do organizowania, więc porządkuje tylko te pola, które są na mapie.

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
źródło
Jeszcze tego nie próbowałem, ale wygląda świetnie. Byłoby jeszcze lepiej, gdyby obsługiwany był również interfejs Marshalera.
przytul się
1

Pytanie jest teraz trochę stare, ale chwilę temu natknąłem się na ten sam problem, a ponieważ nie znalazłem łatwego sposobu, aby to zrobić, zbudowałem bibliotekę spełniającą ten cel. Pozwala w łatwy sposób wygenerować a map[string]interface{}ze struktury statycznej.

https://github.com/tuvistavie/structomap

Daniel Perez
źródło
Możesz to teraz łatwo zrobić, używając fragmentu kodu z mojego przepisu.
Adam Kurkiewicz
Fragment jest podzbiorem biblioteki, ale głównym problemem dotyczącym zwracania a []bytejest to, że nie nadaje się on do ponownego wykorzystania: na przykład nie ma łatwego sposobu na późniejsze dodanie pola. Dlatego sugerowałbym utworzenie map[string]interface{}i pozwolenie części serializacji JSON na standardową bibliotekę.
Daniel Perez
1

Nie miałem tego samego problemu, ale podobny. Poniższy kod rozwiązuje również Twój problem, oczywiście jeśli nie masz nic przeciwko problemowi z wydajnością. Przed wdrożeniem tego typu rozwiązania w swoim systemie, polecam przeprojektowanie struktury, jeśli masz taką możliwość. Wysyłanie odpowiedzi o zmiennej strukturze jest przesadą techniczną. Uważam, że struktura odpowiedzi reprezentuje kontrakt między żądaniem a zasobem i nie powinno to być żądaniami zależnymi (można ustawić niepotrzebne pola na wartość null, tak jest). W niektórych przypadkach musimy wdrożyć ten projekt, jeśli uważasz, że tak jest, oto link do gry i kod, którego używam.

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
źródło
1

Stworzyłem tę funkcję, aby przekonwertować struct na ciąg JSON, ignorując niektóre pola. Mam nadzieję, że to pomoże.

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
}

Przykład: https://play.golang.org/p/nmq7MFF47Gp

Chhaileng
źródło