Golang的JSON解码验证不会在接口中引发错误。

huangapple go评论82阅读模式
英文:

Golang Json decode validation won't raise error with interface

问题

我是一个对Golang完全不熟悉的新手,非常感谢对以下问题的任何帮助。

我有这段代码片段,之前一直正常工作:

var settings CloudSettings

type CloudSettings struct {
...
	A1 *bool `json:"cloud.feature1,omitempty"`
...
}

err = json.NewDecoder(request.Body).Decode(&settings)

尝试发送一个无效的字符串会引发以下错误

curl ... -d '{"cloud.feature1" : "Junk"}'

"message":"Error:strconv.ParseBool: parsing "Junk": invalid syntax Decoding request body."

现在,我们有一个单独的LocalSettings结构体,同一个函数需要有条件地处理云端/本地设置的解码。

所以,代码被修改为:

var settings interface{} = CloudSettings{}

// 如果请求头指定了本地设置
settings = LocalSettings{}

/* 在这个改动之后,Decode()不再对无效字符串引发错误,并接受任何内容 */
err = json.NewDecoder(request.Body).Decode(&settings)

所以问题是,为什么会出现这种行为,我该如何修复?

如果我有两个单独的settings变量,那么从那一点开始的整个代码将会被复制,而我想避免这种情况。

英文:

I am a total noob with Golang and would really appreciate any help on the following

I had this code snippet which was working fine

var settings CloudSettings

type CloudSettings struct {
...
	A1 *bool `json:"cloud.feature1,omitempty"`
...
}

err = json.NewDecoder(request.Body).Decode(&settings)

An attempt to send an invalid string would raise this error:

curl ... -d '{"cloud.feature1" : "Junk"}'

"message":"Error:strconv.ParseBool: parsing \"Junk\": invalid syntax Decoding request body."

Now, we have a separate LocalSettings struct and the same function needs to handle cloud/local setting decoding conditionally

So, the code was changed to:

var settings interface{} = CloudSettings{}

// If the request header says local settings
settings = LocalSettings{}

/* After this change Decode() no longer raises any error for invalid strings and accepts anything */
err = json.NewDecoder(request.Body).Decode(&settings)

So the question is why do I see this behavior and how would I fix this ?

If I have 2 separate settings variables, then the entire code from that point onwards would just be duplicated which I want to avoid

答案1

得分: 2

在第二个代码片段中,你初始化了一个指向结构体的接口,但是传递的是该接口的地址。该接口包含一个LocalSettingsCloudSetting的值,这个值是不能被覆盖的,所以解码器创建了一个map[string]interface{},将传递的接口的值设置为指向该map,并对数据进行解组。当你运行第二个代码片段时,你没有初始化本地设置或云设置。

修改为:

settings = &CloudSettings{}

或者

settings = &LocalSettings{}

以及

err = json.NewDecoder(request.Body).Decode(settings)

这样应该会按预期工作。

英文:

In the second snippet, you have an interface initialized to a struct, but passing address of that interface. The interface contains a LocalSettings or CloudSetting value, which cannot be overwritten, so the decoder creates a map[string]interface{}, sets the value of the passed interface to point to that, and unmarshal data. When you run the second snippet, you are not initializing the local settings or cloud settings.

Change:

settings=&CloudSettings{}

or

settings=&LocalSettings{}

and

err = json.NewDecoder(request.Body).Decode(settings)

and it should behave as expected

答案2

得分: 1

根据你的问题,我假设所有字段(即使名称相同)在JSON标签中都有cloud.local.前缀。如果是这样的话,你可以将两个选项嵌入到一个单一类型中:

type Wrapper struct {
    *CloudSettings
    *LocalSettings
}

然后将其解组为这个包装类型。JSON标签将确保正确的字段在正确的设置类型中填充:

wrapper := &Wrapper{}
if err := json.NewDecoder(request.Body).Decode(&wrapper); err != nil {
    // 处理错误
}
// 现在确定传递了哪些设置:
if wrapper.CloudSettings == nil {
    fmt.Println("提供了本地设置!")
    // 使用 wrapper.CloudSettings
} else {
    fmt.Println("提供了云设置!")
    // 使用 wrapper.LocalSettings
}

Playground演示

你提到我们希望根据标头值加载本地设置。你可以简单地解组有效载荷,然后检查标头是否与加载的设置类型匹配。如果标头指定了本地设置,但有效载荷包含了云设置,只需返回错误响应即可。

不过,我在这里假设你的JSON标签对于两种设置类型是不同的。这并不总是适用,所以如果我的假设是错误的,并且某些字段共享相同的JSON标签,那么自定义的Unmarshal函数将是解决方法:

func (w *Wrapper) UnmarshalJSON(data []byte) error {
    // 假设我们优先使用本地设置:
    l := LocalSettings{}
    if err := json.Unmarshal(data, &l); err == nil {
        // 我们可以顺利解组为本地设置吗?
        w.CloudSettings = nil // 确保这个字段为空
        w.LocalSettings = &l // 设置本地设置
        return nil
    }
    // 我们应该使用云设置
    c := CloudSettings{}
    if err := json.Unmarshal(data, &c); err != nil {
        return err
    }
    w.LocalSettings = nil
    w.CloudSettings = &c
    return nil
}

这样,任何冲突都会得到处理,我们可以控制哪些设置优先。同样,无论JSON解组的结果如何,你只需交叉检查标头值+哪个设置类型被填充,并从那里继续处理。

最后,如果两种设置类型之间有很大的重叠,你也可以将有效载荷解组为两种类型,并在包装类型中填充两个字段:

func (w *Wrapper) UnmarshalJSON(data []byte) error {
    *w = Wrapper{} // 确保我们从一个干净的状态开始
    l := LocalSettings{}
    var localFail error
    if err := json.Unmarshal(data, &l); err == nil {
        w.LocalSettings = &l // 设置本地设置
    } else {
        localFail = err
    }
    c := CloudSettings{}
    if err := json.Unmarshal(data, &c); err == nil {
        w.CloudSettings = &c
    } else if localFail != nil { // 两个解组调用都失败了
        return err // 可能包装/返回自定义错误
    }
    return nil // 一个或多个解组成功
}

这样应该可以解决问题。

英文:

Based on your question, I'm assuming all fields (even the ones with the same name) have a cloud. or local. prefix in the JSON tags. If that's the case, you can simply embed both options into a single type:

type Wrapper struct {
    *CloudSettings
    *LocalSettings
}

Then unmarshal into this wrapper type. The JSON tags will ensure the correct field on the correct settings type are populated:

wrapper := &Wrapper{}
if err := json.NewDecoder(request.Body).Decode(&wrapper); err != nil {
    // handle
}
// now to work out which settings were passed:
if wrapper.CloudSettings == nil {
    fmt.Println("Local settings provided!")
    // use wrapper.CloudSettings
} else {
    fmt.Println("Cloud settings provided!")
    // use wrapper.LocalSettings
}

Playground demo

You mention that we expect to see local settings loaded based on a header value. You can simply unmarshal the payload, and then check whether the header matches the settings type that was loaded. If the header specified local settings, but the payload contained cloud settings, simply return an error response.

Still, I'm assuming here that your JSON tags will be different for both setting types. That doesn't always apply, so if my assumption is incorrect, and some fields share the same JSON tags, then a custom Unmarshal function would be the way to go:

func (w *Wrapper) UnmarshalJSON(data []byte) error {
    // say we want to prioritise Local over cloud:
    l := LocalSettings{}
    if err := json.Unmarshal(data, &l); err == nil {
        // we could unmarshal into local without a hitch?
        w.CloudSettings = nil // ensure this is blanked out
        w.LocalSettings = &l // set local
        return nil
    }
    // we should use cloud settings
    c := CloudSettings{}
    if err := json.Unmarshal(data, &c); err != nil {
        return err
    }
    w.LocalSettings = nil
    w.CloudSettings = &c
    return nil
}

This way, any conflicts are taken care of, and we can control which settings take precedence. Again, regardless of the outcome of the JSON unmarshalling, you can simply cross check the header value + which settings type was populated, and take it from there.

Lastly, if there's a sizeable overlap between both settings types, you could just as well unmarshal the payload into both types, and populate both fields in the wrapper type:

func (w *Wrapper) UnmarshalJSON(data []byte) error {
    *w = Wrapper{} // make sure we start with a clean slate
    l := LocalSettings{}
    var localFail err
    if err := json.Unmarshal(data, &l); err == nil {
        w.LocalSettings = &l // set local
    } else {
        localFail = err
    }
    c := CloudSettings{}
    if err := json.Unmarshal(data, &c); err == nil {
        w.CloudSettings = &c
    } else if localFail != nil { // both unmarshal calls failed
        return err // maybe wrap/return custom error
    }
    return nil // one or more unmarshals were successful
}

That should do the trick

huangapple
  • 本文由 发表于 2022年9月13日 23:54:08
  • 转载请务必保留本文链接:https://go.coder-hub.com/73705950.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定