Eliminar campos de struct u ocultarlos en JSON Response

181

Creé una API en Go que, al ser llamada, realiza una consulta, crea una instancia de una estructura y luego codifica esa estructura como JSON antes de enviarla de nuevo a la persona que llama. Ahora me gustaría permitir que la persona que llama pueda seleccionar los campos específicos que le gustaría devolver al pasar un parámetro GET de "campos".

Esto significa que dependiendo de los valores de los campos, mi estructura cambiaría. ¿Hay alguna forma de eliminar campos de una estructura? ¿O al menos ocultarlos en la respuesta JSON dinámicamente? (Nota: a veces tengo valores vacíos, por lo que la etiqueta omitEmpty de JSON no funcionará aquí) Si ninguno de estos es posible, ¿hay alguna sugerencia sobre una mejor manera de manejar esto? Gracias por adelantado.

A continuación se muestra una versión más pequeña de las estructuras que estoy 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

Luego codifico y envío la respuesta de esta manera:

err := json.NewEncoder(c.ResponseWriter).Encode(&msg)
usuario387049
fuente
77
@Jacob, según la respuesta actualizada de PuerkitoBio, creo que leíste mal la pregunta. La (actualmente) aceptada podría no ser la "respuesta correcta" a su pregunta, ¡pero es la que se pregunta aquí! La respuesta (actualmente) más votada puede responder a su pregunta, ¡pero es completamente inaplicable a esta!
Dave C

Respuestas:

275

EDITAR: Noté algunos votos negativos y eché otro vistazo a estas preguntas y respuestas. La mayoría de las personas parecen perderse que el OP solicitó que los campos se seleccionen dinámicamente según la lista de campos proporcionada por la persona que llama. No puede hacer esto con la etiqueta json struct estáticamente definida.

Si lo que desea es omitir siempre un campo para codificar json, entonces, por supuesto, úselo json:"-"para ignorar el campo (también tenga en cuenta que esto no es necesario si su campo no se exporta; esos campos siempre son ignorados por el codificador json). Pero esa no es la pregunta del OP.

Para citar el comentario sobre la json:"-"respuesta:

Esta [la json:"-"respuesta] es la respuesta que la mayoría de las personas que terminan aquí buscando buscarían, pero no es la respuesta a la pregunta.


En este caso, usaría una interfaz de mapa [cadena] {} en lugar de una estructura. Puede eliminar fácilmente los campos llamando a la función deleteintegrada en el mapa para que se eliminen los campos.

Es decir, si no puede consultar solo los campos solicitados en primer lugar.

mna
fuente
44
lo más probable es que no quieras descartar por completo tu definición de tipo. Eso va a ser molesto en el futuro, como cuando quieres escribir otros métodos en este tipo que accedan a esos campos. Usar un intermedio map[string]interface{}tiene sentido, pero no requiere que deseche su definición de tipo.
jorelli
1
La otra respuesta es la respuesta real a esta pregunta.
Jacob
1
Un posible inconveniente de eliminar es que a veces es posible que desee admitir múltiples vistas json de su estructura (mapa). Por ejemplo, vista json para el cliente sin un campo sensible, y vista json para la base de datos CON el campo sensible. Afortunadamente, todavía es posible usar la estructura, solo eche un vistazo a mi respuesta.
Adam Kurkiewicz
Esto funciona para mí, ya que solo necesitaba un específico Idpero no quiero devolver toda la estructura json. ¡Gracias por esto!
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
fuente
14
No estoy de acuerdo con @Jacob porque el OP dijo que querían controlar dinámicamente los campos de salida en función de las entradas de cadena de consulta a la API. Por ejemplo, si la persona que llama a la API solo solicita Industria y País, entonces deberá eliminar el resto. Es por eso que la respuesta "marcada" se marca como una respuesta a esta pregunta. Esta respuesta altamente votada es para marcar campos explícitamente nunca-disponibles-para-cualquier-builtin-json-marshaler - NUNCA. Si lo desea dinámicamente, la respuesta marcada es la respuesta.
eduncan911
11
Esta es la respuesta que la mayoría de las personas que terminan aquí buscando buscarían, pero no es la respuesta a la pregunta.
Filip Haglund
55
Como ya se dijo, el OP estaba pidiendo un método para formar dinámicamente un DTO.
codepushr
53

Otra forma de hacer esto es tener una estructura de punteros con la ,omitemptyetiqueta. Si los punteros son nulos , los campos no serán Marshalled.

Este método no requerirá una reflexión adicional o un uso ineficiente de los mapas.

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

Druska
fuente
3
+1 Completamente de acuerdo. Uso esta regla / truco todo el tiempo con los marshalers incorporados (¡e incluso construí un lector / escritor CSV basado en esta regla también! El OP podría simplemente no establecer el valor * País en nulo, y se omitiría. Y es asombroso que hayas proporcionado un agradable juego de escribir.golang también.
eduncan911
2
Por supuesto, ese método requiere reflexión, el cálculo de referencias json-a-estructura de stdlib siempre usa reflexión (en realidad siempre usa período de reflexión, mapa o estructura o lo que sea).
mna
Sí, pero no requiere reflexión adicional utilizando interfaces, que recomiendan algunas otras respuestas.
Druska
14

Puede usar el reflectpaquete para seleccionar los campos que desee reflejando en las etiquetas de campo y seleccionando los jsonvalores de las etiquetas. Defina un método en su tipo SearchResults que seleccione los campos que desea y los devuelva como a map[string]interface{}, y luego marque eso en lugar de la estructura SearchResults en sí. Aquí hay un ejemplo de cómo podría definir ese 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
}

y aquí hay una solución ejecutable que muestra cómo llamaría a este método y calificaría su selección: http://play.golang.org/p/1K9xjQRnO8

jorelli
fuente
Ahora que lo pienso, podría generalizar razonablemente el patrón selectfields a cualquier tipo y cualquier clave de etiqueta; No hay nada sobre esto que sea específico para la definición de SearchResult o la clave json.
jorelli
Estoy tratando de mantenerme alejado de la reflexión, pero esto guarda la información de tipo bastante bien ... Es bueno tener un código que documente cómo se ven sus estructuras mejor que un montón de etiquetas if / else en un método validate () tener uno)
Aktau
7

Acabo de publicar Sheriff , que transforma las estructuras en un mapa basado en etiquetas anotadas en los campos de la estructura. Luego puede ordenar (JSON u otros) el mapa generado. Probablemente no le permita serializar solo el conjunto de campos que solicitó la persona que llama, pero imagino que usar un conjunto de grupos le permitiría cubrir la mayoría de los casos. El uso de grupos en lugar de los campos directamente probablemente también aumentaría la capacidad de caché.

Ejemplo:

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

Toma tres ingredientes:

  1. El reflectpaquete para recorrer todos los campos de una estructura.

  2. Una ifdeclaración para recoger los campos que desea Marshaly

  3. El encoding/jsonpaquete a Marshallos campos de tu agrado.

Preparación:

  1. Licuarlos en una buena proporción. Use reflect.TypeOf(your_struct).Field(i).Name()para obtener un nombre del icampo th de your_struct.

  2. Use reflect.ValueOf(your_struct).Field(i)para obtener una Valuerepresentación de tipo de un icampo th de your_struct.

  3. Utilícelo fieldValue.Interface()para recuperar el valor real (incluido en la interfaz de tipo {}) fieldValuedel tipo de Value(tenga en cuenta el uso de corchetes: el método Interface () produceinterface{}

Si afortunadamente logra no quemar ningún transistor o interruptor automático en el proceso, debería obtener algo como esto:

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
}

Servicio:

servir con una estructura arbitraria y una map[string]boolde los campos que desea incluir, por ejemplo

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

}

¡Buen provecho!

Adam Kurkiewicz
fuente
¡Advertencia! Si sus includeFields contienen nombres de campo, que no coinciden con los campos reales, obtendrá un json no válido. Usted ha sido advertido.
Adam Kurkiewicz
5

Puede usar el atributo de etiquetado "omitifempty" o hacer punteros de campo opcionales y dejar los que desea omitir sin inicializar.

deemok
fuente
Esta es la respuesta más correcta a la pregunta de OP y al caso de uso.
user1943442
2
@ user1943442, no lo es; el OP menciona explícitamente por qué "omitempty" no es aplicable.
Dave C
2

También enfrenté este problema, al principio solo quería especializar las respuestas en mi controlador http. Mi primer enfoque fue crear un paquete que copiara la información de una estructura a otra estructura y luego reunir esa segunda estructura. Hice ese paquete usando la reflexión, por lo que nunca me gustó ese enfoque y tampoco lo fui dinámicamente.

Así que decidí modificar el paquete de codificación / json para hacer esto. Las funciones Marshal, MarshalIndenty (Encoder) Encodeademás recibe un

type F map[string]F

Quería simular un JSON de los campos que se necesitan para ordenar, por lo que solo ordena los campos que están en el 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
fuente
No lo he probado todavía, pero esto se ve muy bien. Sería aún mejor si la interfaz Marshaler también es compatible.
Huggie
1

La pregunta ahora es un poco vieja, pero me encontré con el mismo problema hace un tiempo, y como no encontré una manera fácil de hacerlo, construí una biblioteca que cumplía este propósito. Permite generar fácilmente a map[string]interface{}partir de una estructura estática.

https://github.com/tuvistavie/structomap

Daniel Pérez
fuente
Ahora puede hacerlo fácilmente usando un fragmento de código de mi receta.
Adam Kurkiewicz
El fragmento es un subconjunto de la biblioteca, pero un problema importante aquí acerca de devolver un []bytees que no es muy reutilizable: no es una manera fácil de agregar un campo después, por ejemplo. Por lo tanto, sugeriría crear un map[string]interface{}y dejar que la serialización JSON forme parte de la biblioteca estándar.
Daniel Pérez
1

No tuve el mismo problema pero similar. El siguiente código también resuelve su problema, por supuesto si no le importa el problema de rendimiento. Antes de implementar ese tipo de solución en su sistema, le recomiendo que rediseñe su estructura si puede. Enviar respuesta de estructura variable es sobre ingeniería. Creo que una estructura de respuesta representa un contrato entre una solicitud y un recurso y no deberían ser solicitudes dependientes (puede hacer que los campos no deseados sean nulos, lo hago). En algunos casos tenemos que implementar este diseño, si crees que estás en esos casos, aquí está el enlace de reproducción y el código que 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
fuente
1

Creé esta función para convertir la estructura a una cadena JSON ignorando algunos campos. Espero que ayude.

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
}

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

Chhaileng
fuente