Removendo campos de struct ou ocultando-os no JSON Response

181

Criei uma API no Go que, ao ser chamada, executa uma consulta, cria uma instância de uma estrutura e, em seguida, codifica essa estrutura como JSON antes de enviar de volta ao chamador. Agora, eu gostaria de permitir que o chamador pudesse selecionar os campos específicos que eles gostariam de retornar, passando um parâmetro GET "fields".

Isso significa que, dependendo do (s) valor (es) dos campos, minha estrutura mudaria. Existe alguma maneira de remover campos de uma estrutura? Ou pelo menos oculte-os na resposta JSON dinamicamente? (Nota: Às vezes, tenho valores vazios para que a tag JSON omitEmpty não funcione aqui) Se nenhuma dessas opções for possível, há uma sugestão sobre uma maneira melhor de lidar com isso? Desde já, obrigado.

Uma versão menor das estruturas que estou usando está abaixo:

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

Em seguida, codifico e produzo a resposta da seguinte maneira:

err := json.NewEncoder(c.ResponseWriter).Encode(&msg)
user387049
fonte
7
@ Jacob, de acordo com a resposta atualizada do PuerkitoBio, acho que você leu a pergunta errado. O (atualmente) aceito pode não ser a "resposta correta" para sua pergunta, mas é o que é solicitado aqui! A resposta (atualmente) mais votada pode responder à sua pergunta, mas é completamente inaplicável a esta!
Dave C

Respostas:

275

EDIT: Eu notei alguns votos negativos e dei uma outra olhada neste Q&A. Parece que a maioria das pessoas sente falta de que o OP solicitou a seleção dinâmica de campos com base na lista de campos fornecida pelo chamador. Você não pode fazer isso com a tag json struct definida estaticamente.

Se o que você deseja é sempre pular um campo para json-encode, é claro que use json:"-"para ignorar o campo (observe também que isso não será necessário se o seu campo não for exportado - esses campos são sempre ignorados pelo codificador json). Mas essa não é a pergunta do OP.

Para citar o comentário na json:"-"resposta:

Essa [a json:"-"resposta] é a resposta que a maioria das pessoas que acabam aqui de pesquisar deseja, mas não é a resposta para a pergunta.


Eu usaria uma interface de [string] do mapa {} em vez de uma estrutura nesse caso. Você pode remover facilmente os campos chamando o deletebuilt-in no mapa para remover os campos.

Ou seja, se você não puder consultar apenas os campos solicitados em primeiro lugar.

mna
fonte
4
você provavelmente não quer jogar fora totalmente sua definição de tipo. Isso vai ser incômodo no final da linha, como quando você deseja escrever outros métodos nesse tipo que acessem esses campos. Usar um intermediário map[string]interface{}faz sentido, mas não requer que você jogue fora sua definição de tipo.
jorelli
1
A outra resposta é a resposta real a esta pergunta.
24414 Jacob
1
Uma possível desvantagem da exclusão é que às vezes você pode querer suportar várias visualizações json da sua estrutura (mapa). Por exemplo, json view para o cliente sem um campo sensível e json view para o banco de dados WITH o campo sensível. Felizmente, ainda é possível usar a estrutura - basta dar uma olhada na minha resposta.
Adam Kurkiewicz 08/07/2015
Isso funciona para mim, pois eu só precisava de um específico Id, mas não quero retornar toda a estrutura do json. Obrigado por isso!
Louie Miranda
155

use `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
fonte
14
Eu discordaria do @Jacob porque o OP disse que queria controlar dinamicamente os campos de saída com base nas entradas da string de consulta na API. Por exemplo, se o chamador da API solicitar apenas Indústria e País, será necessário remover o restante. É por isso que a resposta "marcada" é marcada como resposta a esta pergunta. Esta resposta altamente votada é para marcar campos explicitamente nunca disponíveis para qualquer json-marshaler embutido - EVER. se você quiser dinamicamente, a resposta marcada é a resposta.
eduncan911
11
Essa é a resposta que a maioria das pessoas que acabam aqui pesquisando gostaria, mas não é a resposta para a pergunta.
Filip Haglund
5
Como já foi dito, o OP estava solicitando um método para formar dinamicamente um DTO.
Codepushr
53

Outra maneira de fazer isso é ter uma estrutura de ponteiros com a ,omitemptytag. Se os ponteiros forem nulos , os campos não serão Marshalled.

Este método não exigirá reflexão adicional ou uso ineficiente de mapas.

Mesmo exemplo que jorelli usando este método: http://play.golang.org/p/JJNa0m2_nw

Druska
fonte
3
+1 Concordo totalmente. Eu uso essa regra / truque o tempo todo com os empacotadores internos (e até construí um leitor / gravador de CSV baseado fora dessa regra também! - eu posso usar código-fonte aberto assim que outro pacote csv go). O PO simplesmente não poderia definir o valor * Country como nulo e seria omitido. E incrível que você forneceu um bom; você digitou play.golang também.
eduncan911
2
É claro que esse método requer reflexão, o empacotamento json-to-struct do stdlib sempre usa reflexão (na verdade, sempre usa período de reflexão, mapa ou estrutura ou qualquer outra coisa).
mna
Sim, mas não requer reflexão adicional usando interfaces, recomendadas por outras respostas.
Druska
14

Você pode usar o reflectpacote para selecionar os campos que deseja, refletindo nas tags de campo e selecionando os jsonvalores da tag. Defina um método no seu tipo SearchResults que selecione os campos que você deseja e os retorne como a map[string]interface{}, e marshal que, em vez de SearchResults, se estruture. Aqui está um exemplo de como você pode definir esse método:

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
}

e aqui está uma solução executável que mostra como você chamaria esse método e organizaria sua seleção: http://play.golang.org/p/1K9xjQRnO8

jorelli
fonte
pensando bem, você poderia razoavelmente generalizar o padrão selectfields para qualquer tipo e qualquer tecla de tag; não há nada específico sobre a definição SearchResult ou a chave json.
jorelli
Estou tentando ficar longe da reflexão, mas isso economiza bastante as informações de tipo ... É bom ter um código que documente como suas estruturas são melhores do que um monte de tags if / else em um método validate () (se você ainda tem um) #
9333 Aktau
7

Acabei de publicar o xerife , que transforma estruturas em um mapa com base em tags anotadas nos campos struct. Você pode empacotar (JSON ou outros) o mapa gerado. Provavelmente, não permite que você serialize apenas o conjunto de campos solicitados pelo chamador, mas imagino que o uso de um conjunto de grupos permita cobrir a maioria dos casos. O uso de grupos em vez dos campos diretamente provavelmente também aumentaria a capacidade do cache.

Exemplo:

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

Tome três ingredientes:

  1. O reflectpacote para fazer um loop em todos os campos de uma estrutura.

  2. Uma ifdeclaração para selecionar os campos que você deseja Marshale

  3. O encoding/jsonpacote para Marshalos campos de sua preferência.

Preparação:

  1. Misture-os em uma boa proporção. Use reflect.TypeOf(your_struct).Field(i).Name()para obter um nome do ith th de your_struct.

  2. Use reflect.ValueOf(your_struct).Field(i)para obter uma Valuerepresentação de tipo de um ith th de your_struct.

  3. Use fieldValue.Interface()para recuperar o valor real (upcasted para digitar interface {}) fieldValuedo tipo Value(observe o uso de colchete - o método Interface () produzinterface{}

Se você conseguir, com sorte, não queimar transistores ou disjuntores no processo, deverá obter algo assim:

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
}

Servindo:

servir com uma estrutura arbitrária e um map[string]booldos campos que você deseja incluir, por exemplo

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

}

Bom apetite!

Adam Kurkiewicz
fonte
Aviso! Se seus includeFields contiverem nomes de campos que não correspondem aos campos reais, você receberá um json inválido. Você foi avisado.
Adam Kurkiewicz
5

Você pode usar o atributo de marcação "omitifempty" ou fazer ponteiros de campos opcionais e deixar aqueles que deseja ignorar não inicializados.

julgar
fonte
Essa é a resposta mais correta para a pergunta e o caso de uso dos OPs.
user1943442
2
@ user1943442, não é; o OP menciona explicitamente por que "omitempty" é inaplicável.
Dave C
2

Eu também enfrentei esse problema, no começo eu só queria especializar as respostas no meu manipulador de http. Minha primeira abordagem foi criar um pacote que copia as informações de uma estrutura para outra estrutura e, em seguida, empacota essa segunda estrutura. Eu fiz esse pacote usando reflexão, então, nunca gostei dessa abordagem e também não fui dinamicamente.

Então, decidi modificar o pacote encoding / json para fazer isso. As funções Marshal, MarshalIndente (Encoder) Encode, adicionalmente, recebe um

type F map[string]F

Eu queria simular um JSON dos campos necessários para empacotar, portanto, ele empacota apenas os campos que estão no mapa.

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
fonte
Ainda não tentei, mas isso parece ótimo. Seria ainda melhor se a interface Marshaler também fosse suportada.
huggie 12/02
1

A questão agora é um pouco antiga, mas me deparei com a mesma questão há pouco tempo e, como não encontrei uma maneira fácil de fazer isso, construí uma biblioteca cumprindo esse objetivo. Permite gerar facilmente ummap[string]interface{} partir de uma estrutura estática.

https://github.com/tuvistavie/structomap

Daniel Perez
fonte
Agora você pode fazer isso facilmente usando um trecho de código da minha receita.
Adam Kurkiewicz
O trecho de código é um subconjunto da biblioteca, mas um dos principais problemas aqui sobre o retorno de um []byteé que ele não é muito reutilizável: não há maneira fácil de adicionar um campo posteriormente, por exemplo. Então, eu sugeriria criar um map[string]interface{}e deixar a serialização JSON fazer parte da biblioteca padrão.
Daniel Perez
1

Eu não tive o mesmo problema, mas semelhante. O código abaixo também resolve seu problema, é claro, se você não se importa com problemas de desempenho. Antes de implementar esse tipo de solução em seu sistema, recomendo que você redesenhe sua estrutura, se puder. O envio da resposta da estrutura variável está com excesso de engenharia. Acredito que uma estrutura de resposta represente um contrato entre uma solicitação e um recurso e não deve ser dependente de solicitações (você pode tornar nulos os campos não desejados). Em alguns casos, temos que implementar esse design, se você acredita que está nesses casos, aqui está o link e o código de reprodução que eu 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))

}
RockOnGom
fonte
1

Criei essa função para converter struct em JSON, ignorando alguns campos. Espero que ajude.

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
}

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

Chhaileng
fonte