如何告诉客户端从Go服务器发送整数而不是字符串?

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

How to tell the client they need to send an integer instead of a string, from a Go server?

问题

假设我在服务器上有以下的Go结构体:

type account struct {
    Name    string
    Balance int
}

我想要在接收到请求时调用json.Decode将其解析为一个account对象:

var ac account
err := json.NewDecoder(r.Body).Decode(&ac)

如果客户端发送以下请求:

{
    "name": "test@example.com",
    "balance": "3"
}

Decode()会返回以下错误:

json: cannot unmarshal string into Go value of type int

现在,可能可以将其解析为"你发送了一个字符串给Balance字段,但实际上应该发送一个整数",但这很棘手,因为你不知道字段的名称。如果请求中有很多字段,情况会变得更加复杂,你不知道哪个字段解析失败了。

在Go中,如何以最佳方式处理这个接收到的请求,并返回错误消息"Balance必须是一个字符串",对于任意数量的整数字段呢?

英文:

Let's say I have the following Go struct on the server

type account struct {
	Name    string
	Balance int
}

I want to call json.Decode on the incoming request to parse it into an account.

	var ac account
	err := json.NewDecoder(r.Body).Decode(&ac)

If the client sends the following request:

{
    "name": "test@example.com", 
    "balance": "3"
}

Decode() will return the following error:

json: cannot unmarshal string into Go value of type int

Now it's possible to parse that back into "you sent a string for Balance, but you really should have sent an integer", but it's tricky, because you don't know the field name. It also gets a lot trickier if you have a lot of fields in the request - you don't know which one failed to parse.

What's the best way to take that incoming request, in Go, and return the error message, "Balance must be a string", for any arbitrary number of integer fields on a request?

答案1

得分: 3

你可以为“Balance”字段使用自定义类型和自定义解组算法。

现在有两种可能性:

  • 处理两种类型:
package main

import (
    "encoding/json"
    "fmt"
    "strconv"
)

type Int int

type account struct {
    Name    string
    Balance Int
}

func (i *Int) UnmarshalJSON(b []byte) (err error) {
    var s string
    err = json.Unmarshal(b, &s)
    if err == nil {
        var n int
        n, err = strconv.Atoi(s)
        if err != nil {
            return
        }
        *i = Int(n)
        return
    }

    var n int
    err = json.Unmarshal(b, &n)
    if err == nil {
        *i = Int(n)
    }
    return
}

func main() {
    for _, in := range [...]string{
        `{"Name": "foo", "Balance": 42}`,
        `{"Name": "foo", "Balance": "111"}`,
    } {
        var a account
        err := json.Unmarshal([]byte(in), &a)
        if err != nil {
            fmt.Printf("Error decoding JSON: %v\n", err)
        } else {
            fmt.Printf("Decoded OK: %v\n", a)
        }
    }
}

Playground链接

  • 只处理数值类型,并对其他类型返回一个合理的错误:
package main

import (
    "encoding/json"
    "fmt"
)

type Int int

type account struct {
    Name    string
    Balance Int
}

type FormatError struct {
    Want   string
    Got    string
    Offset int64
}

func (fe *FormatError) Error() string {
    return fmt.Sprintf("Invalid data format at %d: want: %s, got: %s",
        fe.Offset, fe.Want, fe.Got)
}

func (i *Int) UnmarshalJSON(b []byte) (err error) {
    var n int
    err = json.Unmarshal(b, &n)
    if err == nil {
        *i = Int(n)
        return
    }
    if ute, ok := err.(*json.UnmarshalTypeError); ok {
        err = &FormatError{
            Want:   "number",
            Got:    ute.Value,
            Offset: ute.Offset,
        }
    }
    return
}

func main() {
    for _, in := range [...]string{
        `{"Name": "foo", "Balance": 42}`,
        `{"Name": "foo", "Balance": "111"}`,
    } {
        var a account
        err := json.Unmarshal([]byte(in), &a)
        if err != nil {
            fmt.Printf("Error decoding JSON: %#v\n", err)
            fmt.Printf("Error decoding JSON: %v\n", err)
        } else {
            fmt.Printf("Decoded OK: %v\n", a)
        }
    }
}

Playground链接

还有第三种可能性:为整个account类型编写自定义解组器,但这需要更复杂的代码,因为您需要使用encoding/json.Decoder类型的方法实际迭代输入的JSON数据。

仔细阅读您的问题后,我承认除非您愿意使用支持通过JSON模式进行验证的第三方包(我认为我会首先看看this,因为juju是一个相当成熟的产品),否则为整个类型编写自定义解析器是唯一明智的选择。

英文:

You can use a custom type with custom unmarshaling algorythm for your "Balance" field.

Now there are two possibilities:

  • Handle both types:

      package main
    import (
    "encoding/json"
    "fmt"
    "strconv"
    )
    type Int int
    type account struct {
    Name    string
    Balance Int
    }
    func (i *Int) UnmarshalJSON(b []byte) (err error) {
    var s string
    err = json.Unmarshal(b, &s)
    if err == nil {
    var n int
    n, err = strconv.Atoi(s)
    if err != nil {
    return
    }
    *i = Int(n)
    return
    }
    var n int
    err = json.Unmarshal(b, &n)
    if err == nil {
    *i = Int(n)
    }
    return
    }
    func main() {
    for _, in := range [...]string{
    `{"Name": "foo", "Balance": 42}`,
    `{"Name": "foo", "Balance": "111"}`,
    } {
    var a account
    err := json.Unmarshal([]byte(in), &a)
    if err != nil {
    fmt.Printf("Error decoding JSON: %v\n", err)
    } else {
    fmt.Printf("Decoded OK: %v\n", a)
    }
    }
    }
    

    Playground link.

  • Handle only a numeric type, and fail anything else with a sensible error:

      package main
    import (
    "encoding/json"
    "fmt"
    )
    type Int int
    type account struct {
    Name    string
    Balance Int
    }
    type FormatError struct {
    Want   string
    Got    string
    Offset int64
    }
    func (fe *FormatError) Error() string {
    return fmt.Sprintf("Invalid data format at %d: want: %s, got: %s",
    fe.Offset, fe.Want, fe.Got)
    }
    func (i *Int) UnmarshalJSON(b []byte) (err error) {
    var n int
    err = json.Unmarshal(b, &n)
    if err == nil {
    *i = Int(n)
    return
    }
    if ute, ok := err.(*json.UnmarshalTypeError); ok {
    err = &FormatError{
    Want:   "number",
    Got:    ute.Value,
    Offset: ute.Offset,
    }
    }
    return
    }
    func main() {
    for _, in := range [...]string{
    `{"Name": "foo", "Balance": 42}`,
    `{"Name": "foo", "Balance": "111"}`,
    } {
    var a account
    err := json.Unmarshal([]byte(in), &a)
    if err != nil {
    fmt.Printf("Error decoding JSON: %#v\n", err)
    fmt.Printf("Error decoding JSON: %v\n", err)
    } else {
    fmt.Printf("Decoded OK: %v\n", a)
    }
    }
    }
    

    Playground link.

There is a third possibility: write custom unmarshaler for the whole account type, but it requires more involved code because you'd need to actually iterate over the input JSON data using the methods of the
encoding/json.Decoder type.

After reading your

> What's the best way to take that incoming request, in Go, and return the error message, "Balance must be a string", for any arbitrary number of integer fields on a request?

more carefully, I admit having a custom parser for the whole type is the only sensible possibility unless you are OK with a 3rd-party package implementing a parser supporting validation via JSON schema (I think I'd look at this first as juju is a quite established product).

答案2

得分: 2

这个问题的解决方案可以使用类型断言,通过使用映射将JSON数据解组为以下结构:

type account struct {
    Name    string
    Balance int
}

var str = `{
    "name": "test@example.com",
    "balance": "3"
}`

func main() {
    var testing = map[string]interface{}{}
    err := json.Unmarshal([]byte(str), &testing)
    if err != nil {
        fmt.Println(err)
    }

    val, ok := testing["balance"]
    if !ok {
        fmt.Println("missing field balance")
        return
    }

    nv, ok := val.(float64)
    if !ok {
        fmt.Println("balance should be a number")
        return
    }

    fmt.Printf("%+v\n", nv)
}

这里使用float64进行类型断言,因为它是Go的JSON解码器支持的默认数字类型。

需要注意的是,使用interface{}可能不值得麻烦。

UnmarshalTypeError(https://golang.org/pkg/encoding/json/#UnmarshalFieldError)包含一个Offset字段,可以用来获取触发错误的JSON数据的内容。

例如,你可以返回以下类型的消息:

无法将字符串解组为类型为int的Go值,附近的"balance": "3"

英文:

A solution for this could be to use a type assertion by using a map to unmarshal the JSON data into:

type account struct {
Name    string
Balance int
}
var str = `{
"name": "test@example.com", 
"balance": "3"
}`
func main() {
var testing = map[string]interface{}{}
err := json.Unmarshal([]byte(str), &testing)
if err != nil {
fmt.Println(err)
}
val, ok := testing["balance"]
if !ok {
fmt.Println("missing field balance")
return
}
nv, ok := val.(float64)
if !ok {
fmt.Println("balance should be a number")
return
}
fmt.Printf("%+v\n", nv)
}

See http://play.golang.org/p/iV7Qa1RrQZ

The type assertion here is done using float64 because it is the default number type supported by Go's JSON decoder.

It should be noted that this use of interface{} is probably not worth the trouble.

The UnmarshalTypeError (https://golang.org/pkg/encoding/json/#UnmarshalFieldError) contains an Offset field that could allow retrieving the contents of the JSON data that triggered the error.

You could for example return a message of the sort:

cannot unmarshal string into Go value of type int near `"balance": "3"`

答案3

得分: 0

似乎这里提供了一个仅适用于Go语言的解决此问题的实现。

type account struct {
    Name    string
    Balance int `json:",string"`
}

在我看来,更正确和可持续的方法是你创建一个类似JavaScript的客户端库,并将其发布到NPM注册表供他人使用(私有仓库也可以)。通过提供这个库,你可以有意义地为消费者定制API,并防止错误渗入你的主程序。

英文:

It would seem that here provides an implementation to work around this issue in Go only.

type account struct {
Name    string
Balance int `json:",string"`
}

In my estimation the more correct and sustainable approach is for you to create a client library in something like JavaScript and publish it into the NPM registry for others to use (private repository would work the same way). By providing this library you can tailor the API for the consumers in a meaningful way and prevent errors creeping into your main program.

huangapple
  • 本文由 发表于 2016年2月10日 00:41:16
  • 转载请务必保留本文链接:https://go.coder-hub.com/35297426.html
匿名

发表评论

匿名网友

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

确定