从结构中删除字段或将它们隐藏在 JSON 响应中

Posted

技术标签:

【中文标题】从结构中删除字段或将它们隐藏在 JSON 响应中【英文标题】:Removing fields from struct or hiding them in JSON Response 【发布时间】:2013-06-22 18:47:14 【问题描述】:

我在 Go 中创建了一个 API,它在被调用时执行查询,创建一个结构的实例,然后将该结构编码为 JSON,然后再发送回调用者。我现在想让调用者能够通过传入“字段”GET 参数来选择他们想要返回的特定字段。

这意味着根据字段值,我的结构会改变。有没有办法从结构中删除字段?或者至少动态地将它们隐藏在 JSON 响应中? (注意:有时我有空值,所以 JSON omitEmpty 标记在这里不起作用)如果这些都不可能,有没有更好的处理方法的建议?

我正在使用的结构的较小版本如下:

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

然后我像这样编码并输出响应:

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

【问题讨论】:

@Jacob,根据 PuerkitoBio 的更新答案,我认为您误读了这个问题。 (当前)接受的可能不是 your 问题的“正确答案”,而是此处提出的问题! (当前)投票最高的答案可能会回答您的问题,但完全不适用于这个问题! 【参考方案1】:

问题是要求根据调用方提供的字段列表动态选择字段。使用静态定义的 json 结构标记无法做到这一点。

如果您想要始终跳过一个字段以进行 json 编码,那么当然可以使用 json:"-" 忽略该字段。 (另请注意,如果您的字段未导出,则不需要;这些字段总是被 json 编码器忽略。)这不是问题要问的。

引用json:"-"回复的评论:

这个 [json:"-" 答案] 是大多数人从搜索中最终到达这里的答案,但这不是问题的答案。

在这种情况下,我会使用 map[string]interface 而不是结构。您可以通过调用地图上的内置delete 来轻松删除字段以删除字段。

也就是说,如果您一开始就不能只查询请求的字段。

【讨论】:

您很可能不想完全放弃您的类型定义。这会很麻烦,比如当你想在这种类型上编写访问这些字段的其他方法时。使用中间的map[string]interface 确实有意义,但它不需要你丢弃你的类型定义。 另一个答案是这个问题的实际答案。 delete 的一个可能缺点是您有时可能希望支持结构(映射)的多个 json 视图。例如,没有敏感字段的客户端的 json 视图,以及带有敏感字段的数据库的 json 视图。幸运的是,仍然可以使用该结构——看看我的答案。 这对我有用,因为我只需要一个特定的 Id 但是,不想返回整个 json 结构。谢谢! 标签只是反射的一部分,这就是它用于编码/解码 JSON 数据的方式。您能否动态更改反射并以这种方式执行 OP 想要的操作?【参考方案2】:

使用`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"`

文档:http://golang.org/pkg/encoding/json/#Marshal

【讨论】:

我不同意@Jacob,因为 OP 说他们希望根据 API 的查询字符串条目动态控制输出字段。例如,如果 API 的调用者只询问 Industry 和 Country,那么您需要删除其余部分。这就是为什么“打勾”的答案被标记为这个问题的答案。这个高度投票的答案是用于明确标记字段永远不可用到任何内置 json-marshaler - 永远。如果你想要动态的,勾选的答案就是答案。 这是大多数人最终从搜索到这里的答案,但不是问题的答案。 如前所述,OP 要求一种动态形成 DTO 的方法。【参考方案3】:

另一种方法是使用带有,omitempty 标记的指针 结构。如果指针为 nil,则不会编组字段。

此方法不需要额外的反射或低效使用地图。

与 jorelli 使用此方法的示例相同:http://play.golang.org/p/JJNa0m2_nw

【讨论】:

+1 完全同意。我一直使用这个规则/技巧和内置的封送处理程序(甚至还根据这个规则构建了一个 CSV 读取器/写入器!-我可能会在另一个 csv go 包的时候开源它)。然后 OP 可以简单地不将 *Country 值设置为 nil,并将其省略。真棒,你提供了一个很好的;你也输入了 play.golang。 当然该方法需要反射,stdlib的json-to-struct封送总是使用反射(实际上它总是使用反射周期、映射或结构等)。 是的,但它不需要使用接口进行附加反射,其他一些答案建议这样做。【参考方案4】:

您可以使用reflect 包通过反映字段标记并选择json 标记值来选择所需的字段。在您的 SearchResults 类型上定义一个方法,该方法选择您想要的字段并将它们作为 map[string]interface 返回,然后编组 that 而不是 SearchResults 结构本身。下面是一个如何定义该方法的示例:

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

这是一个可运行的解决方案,它显示了您将如何调用此方法并编组您的选择:http://play.golang.org/p/1K9xjQRnO8

【讨论】:

想一想,您可以合理地将选择字段模式推广到任何类型和任何标签键;这与 SearchResult 定义或 json 键无关。 我试图远离反射,但这可以很好地保存类型信息......很高兴有代码记录你的结构看起来比一堆 if/else 标签更好validate() 方法(如果你有的话)【参考方案5】:

我刚刚发布了sheriff,它根据结构字段上注释的标签将结构转换为映射。然后,您可以编组(JSON 或其他)生成的地图。它可能不允许您仅序列化调用者请求的字段集,但我想使用一组组可以让您涵盖大多数情况。直接使用组而不是字段很可能还会增加缓存能力。

例子:

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)

【讨论】:

【参考方案6】:

取三种成分:

    reflect 包循环遍历结构的所有字段。

    if 语句用于获取您想要的字段Marshal,并且

    encoding/json 包到 Marshal 您喜欢的字段。

准备工作:

    将它们按比例混合。使用reflect.TypeOf(your_struct).Field(i).Name() 获取your_structith 字段的名称。

    使用reflect.ValueOf(your_struct).Field(i) 获取Value 类型的i 字段your_struct 的表示形式。

    使用fieldValue.Interface() 检索Value 类型的fieldValue 的实际值(向上转换为interface 类型)(注意括号的使用 - Interface() 方法 产生interface

如果你幸运地在这个过程中没有烧毁任何晶体管或断路器,你应该得到这样的东西:

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

服务:

例如,使用任意结构和 map[string]bool 要包含的字段提供服务

type magic struct 
    Magic1 int
    Magic2 string
    Magic3 [2]int


func main() 
    var magic = magic0, "tusia", [2]int0, 1
    if json, status := MarshalOnlyFields(magic, map[string]bool"Magic1": true); status != nil 
        println("error")
     else 
        fmt.Println(string(json))
    


胃口大开!

【讨论】:

警告!如果您的 includeFields 包含与实际字段不匹配的字段名称,您将获得无效的 json。您已收到警告。【参考方案7】:

您可以使用标记属性“omitifempty”或制作可选字段指针,并使您希望跳过的那些未初始化。

【讨论】:

这是对 OP 问题和用例的最正确答案。 @user1943442,不是它不是; OP 明确提到了为什么“omitempty”不适用。【参考方案8】:

我创建了这个函数来通过忽略一些字段将结构转换为 JSON 字符串。希望它会有所帮助。

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

示例:https://play.golang.org/p/nmq7MFF47Gp

【讨论】:

感谢您没有过度设计活生生的 jeebers 的问题。您可能不需要执行 marshal/map/marshal 业务,但很好的解决方案!【参考方案9】:

我没有遇到同样的问题,但类似。当然,如果您不介意性能问题,下面的代码也可以解决您的问题。在对您的系统实施这种解决方案之前,我建议您尽可能重新设计您的结构。发送可变结构响应是过度设计的。我相信响应结构代表请求和资源之间的合同,它不应该是依赖请求。(你可以让不需要的字段为空,我这样做)。在某些情况下,我们必须实现这种设计,如果您认为自己在这种情况下,这里是 play link 和我使用的代码。

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 := UserID: 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 := User2ID: 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))


【讨论】:

【参考方案10】:

这是我定义结构的方式。

type User struct 
    Username string  `json:"username" bson:"username"`
    Email    string  `json:"email" bson:"email"`
    Password *string `json:"password,omitempty" bson:"password"`
    FullName string  `json:"fullname" bson:"fullname"`

在我的函数中设置 user.Password = nil 不被编组。

【讨论】:

【参考方案11】:

我也遇到过这个问题,起初我只是想在我的 http 处理程序中专门处理响应。我的第一种方法是创建一个包,将结构的信息复制到另一个结构,然后编组第二个结构。我使用反射完成了那个包,所以从不喜欢这种方法,而且我也不是动态的。

所以我决定修改 encoding/json 包来做到这一点。函数MarshalMarshalIndent(Encoder) Encode 额外接收一个

type F map[string]F

我想模拟编组所需字段的 JSON,因此它只编组地图中的字段。

https://github.com/jtorz/jsont

package main

import (
    "fmt"
    "log"
    "net/http"

    "github.com/jtorz/jsont/v2"
)

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)

【讨论】:

我还没试过,但这看起来很棒。如果还支持 Marshaler 接口就更好了。【参考方案12】:

这个问题现在有点老了,但不久前我遇到了同样的问题,因为我发现没有简单的方法来做到这一点,所以我建立了一个库来实现这个目的。 它允许从静态结构轻松生成map[string]interface

https://github.com/tuvistavie/structomap

【讨论】:

您现在可以使用我的食谱中的代码 sn-p 轻松完成。 sn-p 是库的一个子集,但是这里关于返回 []byte 的一个主要问题是它不是很可重用:例如,没有简单的方法可以在之后添加一个字段。所以我建议创建一个map[string]interface 并将JSON序列化部分放到标准库中。

以上是关于从结构中删除字段或将它们隐藏在 JSON 响应中的主要内容,如果未能解决你的问题,请参考以下文章

C++ 为啥基类/结构构造函数不能有多个参数可以从派生中隐式调用?

使用 Http Post 发送图像

serialize/json_encode 到一个字段或将原始数据保留在 db 的多个字段中?

如何使用 Akka HTTP 解组 json 响应删除不必要的字段

如何从 JSON 响应中获取字段值

使用 AFNetworking 从 JSON 响应中解析 HTML 标签