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

286

我在Go语言中创建了一个API,当被调用时会执行查询操作、创建一个结构体实例,并将该结构体编码为JSON后发送回调用方。现在,我希望允许调用方通过传递“fields”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)

7
根据PuerkitoBio最新的答案更新,我认为你误读了问题,(目前)被接受的答案可能不是你的问题的“正确答案”,但它是这个问题所问的!(目前)得到最高投票的答案可能回答了你的问题,但对于这个问题来说是完全不适用的!@Jacob - Dave C
15个回答

465
问题要求根据调用者提供的字段列表动态选择字段,使用静态定义的json结构标记无法做到这一点。
如果您想总是跳过一个字段进行json编码,那么当然可以使用“json:"-"”来忽略该字段。(注意,如果您的字段未公开,则始终会被json编码器忽略,因此不需要这样做。) 这不是问题所问的。
引用对“json:"-"”答案的评论: 这[“json:-”答案]是大多数搜索到这里的人想要的答案,但它不是问题的答案。
在这种情况下,我会使用map[string]interface{}而不是结构体。您可以通过调用地图上的内置函数delete来轻松删除要删除的字段。
也就是说,如果您一开始无法仅查询所请求的字段。

9
您很可能不想完全放弃您的类型定义。这会在以后写访问这些字段的其他方法时带来问题。使用一个中间的map[string]interface{}是有意义的,但这并不需要放弃您的类型定义。 - jorelli
2
另一个答案才是这个问题的真正答案。 - Jay
1
delete 的一个可能的缺点是,有时您可能希望支持结构体(映射)的多个 JSON 视图。例如,客户端的 JSON 视图没有敏感字段,而数据库的 JSON 视图则包含敏感字段。幸运的是,仍然可以使用结构体——只需查看我的答案即可。 - Adam Kurkiewicz
一个标签只是反射的一部分,这就是它用于编码/解码JSON数据的方式。你能动态地改变反射并以这种方式实现OP想要的吗? - mandarin
“-”标准是从哪里来的? 是来自于 Go 语言还是一些框架,如 Gin? - Eric
显示剩余2条评论

239

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


20
我不同意@Jacob的观点,因为原帖中说他们想根据API查询字符串条目动态控制输出字段。例如,如果调用者只请求行业和国家,那么您需要删除其余字段。这就是为什么"打勾"答案被标记为该问题的答案。这个得到高票的答案是为了明确标记一些字段永远不会被任何内置JSON编组器使用-从不。如果您想要动态控制,"打勾"答案就是答案。 - eduncan911
25
这是大多数通过搜索到达此处的人想要的答案,但它并不是问题的答案。 - Filip Haglund
5
OP已经表明需要一种动态生成DTO的方法。 - user986408

82
另一种做法是使用带有,omitempty 标签的指针结构体。如果指针为 nil,则该字段不会被编组化。
这种方法不需要额外的反射或低效的映射使用。
使用此方法的示例与 jorelli 相同:http://play.golang.org/p/JJNa0m2_nw

3
完全同意。我经常使用这个规则/技巧来处理内置的序列化程序(甚至基于这个规则构建了一个 CSV 读写器!- 我可能会很快公开作为另一个 Go csv 包)。然后,原帖作者就可以不将 *Country 值设置为 nil,这样它就会被省略。非常感谢您提供了一个类型良好的 play.golang 示例。 - eduncan911
2
当然,该方法需要反射,stdlib的json-to-struct编组始终使用反射(实际上它总是使用反射周期,映射或结构体等)。 - mna
2
是的,但它不需要使用接口进行额外的反射,而其他一些答案建议这样做。 - Druska
1
这是动态显示字段的最佳方式。 - Pedro Fontes
1
这是我个人认为的最佳答案。 - Asteriskdev

20

您可以使用reflect包通过反射字段标签并选择json标记值来选择所需的字段。在您的SearchResults类型上定义一个方法,该方法选择您想要的字段并将它们作为map[string]interface{}返回,然后编组而不是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


说起来,你可以合理地将selectfields模式推广到任何类型和任何标签键;这与SearchResult定义或json键无关。 - jorelli
1
我试图避免使用反射,但这样可以很好地保存类型信息...拥有能够更好地记录结构的代码比在validate()方法中使用一堆if/else标签(如果你真的有这个方法)更好。 - Aktau

9

需要三种成分:

  1. 使用 reflect 包循环访问结构体的所有字段。

  2. 使用 if 语句选择要 Marshal 的字段。

  3. 使用 encoding/json 包将您喜欢的字段进行 Marshal

准备工作:

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

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

  3. 使用 fieldValue.Interface() 检索 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 = 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))
    }

}
< p > 食欲大开!< /p >

1
警告!如果您的includeFields包含字段名称,这些字段名称与实际字段不匹配,那么您将得到一个无效的json。你已经被警告了。 - Adam Kurkiewicz

9
我刚刚发布了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)
}

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

2
赞赏你没有过度工程化解决问题。你可能不需要进行marshal/map/marshal操作,但是这是一个好的解决方案! - Sam Hughes

6

这是我定义结构的方法。

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 来避免被序列化。


5
您可以使用标记属性“omitifempty”,或将可选字段指针留空以跳过您不想要的字段。

这是对于OP的问题和使用情况最正确的答案。 - user1943442
4
@user1943442,不是这样的;OP 明确地提到了为什么“omitempty”不适用。 - Dave C

3
我没有同样的问题,但类似。下面的代码也可以解决你的问题,当然如果你不介意性能问题的话。在实施这种解决方案之前,我建议你重新设计你的结构(如果可能的话)。发送变量结构响应是过度设计。我相信响应结构代表请求和资源之间的契约,它不应该依赖请求。(你可以将不需要的字段设置为null,我也是这样做的)。在某些情况下,我们必须实现此设计,如果你认为你处于这种情况,请使用以下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 := 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))

}

反射是慢的。 - Eric

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接