如何检查 json 是不是与结构/结构字段匹配

Posted

技术标签:

【中文标题】如何检查 json 是不是与结构/结构字段匹配【英文标题】:How to check if a json matches a struct / struct fields如何检查 json 是否与结构/结构字段匹配 【发布时间】:2018-09-10 02:53:35 【问题描述】:

是否有一种简单的方法可以检查 myStruct 的每个字段是否使用 json.Unmarshal(jsonData, &myStruct) 进行映射。

我可以想象的唯一方法是将结构的每个字段定义为指针,否则您将始终返回一个初始化的结构。 因此,每个作为对象的 jsonString(即使是空的 )都会返回一个初始化的结构,而您无法判断该 json 是否代表您的结构。

我能想到的唯一解决方案是很不舒服:

package main

import (
    "encoding/json"
    "fmt"
)

type Person struct 
    Name *string `json:name`
    Age  *int    `json:age`
    Male *bool   `json:male`


func main() 
    var p *Person
    err := json.Unmarshal([]byte(""), &p)
    // handle parse error
    if err != nil 
        return
    

    // handle json did not match error
    if p.Name == nil || p.Age == nil || p.Male == nil 
        return
    

    // now use the fields with dereferencing and hope you did not forget a nil check
    fmt.Println("Hello " + *p.Name)

也许可以使用govalidator 之类的库并使用SetFieldsRequiredByDefault。但是您仍然必须执行验证,并且您仍然需要对整个指针取消引用以进行值检索以及 nil 指针的风险。

我想要的是一个函数,如果字段不匹配,它可以将我未编组的 json 作为结构或错误返回。 golang json 库提供的唯一功能是在未知字段上失败但在缺少字段时不会失败。

有什么想法吗?

【问题讨论】:

您的解决方案有什么问题? Go 并不是要为每个可以封装成函数的用例提供魔法。编写一个可以做一些工作的函数并使用这个函数并不是“不舒服”。那么问题是什么? 我认为它不是很方便,而且我还没有看到很多只使用指向结构字段的指针,以及 nil 指针的所有取消引用和风险。关于魔法:“DisallowUnknownFields”是 json 标准库的一部分,那么为什么“DisallowMissingFields”不能呢?而且我不知道一个函数会是什么样子,它可以为所有结构通用地解决这个问题(因为我正在构建一个库)。 首先:指针在 Go 中并不少见。第二:将其包装成一个函数。您无需使用指针,您所要做的就是在解码期间使用它们,然后复制回“无指针”结构。很简单。 所以通用库的每个用户都必须提供两个结构,必须进行字段 == nil 检查,然后必须复制到另一个结构......非常直观。 :-) 【参考方案1】:

另一种方法是实现您自己的json.Unmarshaler,它使用反射(类似于默认的 json unmarshaler):

有几点需要考虑:

如果速度对您来说非常重要,那么您应该编写一个基准来查看额外反射的影响有多大。我怀疑它可以忽略不计,但编写一个小的 go 基准来获得一些数字也无妨。 stdlib 会将 json 输入中的所有数字解组为浮点数。所以如果你使用反射来设置整数字段,那么你需要自己提供相应的转换(见下例中的TODO) json.Decoder.DisallowUnknownFields 函数无法按预期与您的类型一起使用。您需要自己实现(参见下面的示例) 如果您决定采用这种方法,您将使您的代码更复杂,从而更难理解和维护。你真的确定你必须知道是否省略了字段吗?也许您可以重构您的字段以充分利用零值?

这里是这种方法的完全可执行测试:

package sandbox

import (
    "encoding/json"
    "errors"
    "reflect"
    "strings"
    "testing"
)

type Person struct 
    Name string
    City string


func (p *Person) UnmarshalJSON(data []byte) error 
    var m map[string]interface
    err := json.Unmarshal(data, &m)
    if err != nil 
        return err
    

    v := reflect.ValueOf(p).Elem()
    t := v.Type()

    var missing []string
    for i := 0; i < t.NumField(); i++ 
        field := t.Field(i)
        val, ok := m[field.Name]
        delete(m, field.Name)
        if !ok 
            missing = append(missing, field.Name)
            continue
        

        switch field.Type.Kind() 
        // TODO: if the field is an integer you need to transform the val from float
        default:
            v.Field(i).Set(reflect.ValueOf(val))
        
    

    if len(missing) > 0 
        return errors.New("missing fields: " + strings.Join(missing, ", "))
    

    if len(m) > 0 
        extra := make([]string, 0, len(m))
        for field := range m 
            extra = append(extra, field)
        
        // TODO: consider sorting the output to get deterministic errors:
        // sort.Strings(extra)
        return errors.New("unknown fields: " + strings.Join(extra, ", "))
    

    return nil


func TestJSONDecoder(t *testing.T) 
    cases := map[string]struct 
        in       string
        err      string
        expected Person
    
        "Empty object": 
            in:       ``,
            err:      "missing fields: Name, City",
            expected: Person,
        ,
        "Name missing": 
            in:       `"City": "Berlin"`,
            err:      "missing fields: Name",
            expected: PersonCity: "Berlin",
        ,
        "Age missing": 
            in:       `"Name": "Friedrich"`,
            err:      "missing fields: City",
            expected: PersonName: "Friedrich",
        ,
        "Unknown field": 
            in:       `"Name": "Friedrich", "City": "Berlin", "Test": true`,
            err:      "unknown fields: Test",
            expected: PersonName: "Friedrich", City: "Berlin",
        ,
        "OK": 
            in:       `"Name": "Friedrich", "City": "Berlin"`,
            expected: PersonName: "Friedrich", City: "Berlin",
        ,
    

    for name, c := range cases 
        t.Run(name, func(t *testing.T) 
            var actual Person
            r := strings.NewReader(c.in)
            err := json.NewDecoder(r).Decode(&actual)
            switch 
            case err != nil && c.err == "":
                t.Errorf("Expected no error but go %v", err)
            case err == nil && c.err != "":
                t.Errorf("Did not return expected error %v", c.err)
            case err != nil && err.Error() != c.err:
                t.Errorf("Expected error %q but got %v", c.err, err)
            

            if !reflect.DeepEqual(c.expected, actual) 
                t.Errorf("\nWant: %+v\nGot:  %+v", c.expected, actual)
            
        )
    

【讨论】:

这也可以是通用的,还是您需要为每个结构实现它。由于该库是通用的(类似于 json.Unmarshal )并且该库的用户提供任意结构。 此代码原则上是通用的,但您始终需要为每种类型创建自定义 JSONUnmarshal 函数。您应该能够将我提供的示例代码提取到一个单独的函数中,您可以调用该函数,但一般来说,我建议您谨慎使用这种方法,并且仅在您绝对必须这样做并且默认为标准解组时才使用。请记住,您必须注意我对解组数字类型的评论。此外,我没有测试未导出或匿名字段,因此如果您尝试这种方法,您应该确保也测试这些字段。 为什么需要switch field.Type.Kind() 代码块? 你不需要它。我的示例中的意图只是表明如果您想为特定类型应用逻辑,您可以使用它(请参阅代码中的注释)【参考方案2】:

您可以将p 与空结构进行比较,而不是将每个字段与nil 进行比较。

// handle json did not match error
if p == Person 
    return

由于Person 将使用每个字段的0 值进行初始化,这将导致pointers 的每个属性为nilstrings 将为""ints 将为@987654330 @,等等。

【讨论】:

如果 nil 检查,这至少解决了一点问题。但我仍然认为用原始值的指针搞乱并不好玩。 但是你为什么使用指针而不是值呢?只需在需要时将p 作为指针传递?在我看来,使用值更安全,因为它总是有一个值(初始化时为 0 值 - 你可以检查它)这将防止恐慌访问 nil 指针。 顺便说一句:如果字段可以映射,这只会检查是否 none。您无法从结果中决定是否可以映射所有字段或仅部分字段,因为在这两种情况下检查都是“假”。 这不区分缺失字段和当前零值。

以上是关于如何检查 json 是不是与结构/结构字段匹配的主要内容,如果未能解决你的问题,请参考以下文章

如何在json数组结构中添加附加字段

当 JSON 字段键是日期时,如何将 JSON 对象解组为 Golang 结构?

如何在 JSON 结构中自动添加字段

Composer 的结构详解

如何在 GO 结构中处理来自 JSON 的日期字段

如何修改切片中结构的字段?