如何检查JSON是否匹配结构 /结构字段



是否有一种简单的方法可以通过使用json.unmarshal(jsondata,& mystruct(检查myStruct的每个字段。

我能够映像的唯一方法是将结构的每个字段定义为指针,否则您将始终获得初始化的结构。因此,每个是一个对象(即使是一个空的{}(的每个jsonstration都会返回初始化的struct,您无法分辨JSON是否表示您的struct。

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

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。但是,然后您仍然必须执行验证,但您仍然留下整个指针取消价值检索和零指针的风险。

我想要的是一个函数,如果字段不匹配,则返回我的未贴上的JSON作为结构或错误。Golang JSON库提供的唯一一件事是在未知字段上失败但不会在缺失字段上失败的选择。

有什么想法?

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

有几点要考虑:

  • 如果速度对您来说非常重要,那么您应该写一个基准测试,以了解额外反射的影响有多大。我怀疑它可以忽略不计,但是写一个小的基准来获得一些数字没有什么伤害。
  • stdlib将把JSON输入中的所有数字转换为浮点。因此,如果您使用反射设置整数字段,则需要自己提供相应的转换(请参见下面的示例(
  • 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: Person{City: "Berlin"},
        },
        "Age missing": {
            in:       `{"Name": "Friedrich"}`,
            err:      "missing fields: City",
            expected: Person{Name: "Friedrich"},
        },
        "Unknown field": {
            in:       `{"Name": "Friedrich", "City": "Berlin", "Test": true}`,
            err:      "unknown fields: Test",
            expected: Person{Name: "Friedrich", City: "Berlin"},
        },
        "OK": {
            in:       `{"Name": "Friedrich", "City": "Berlin"}`,
            expected: Person{Name: "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: %+vnGot:  %+v", c.expected, actual)
            }
        })
    }
}

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

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

由于Person{}将以每个字段的0值初始化,因此这将导致每个属性为pointersnilstrings将为""ints将为0,等等。

最新更新