英文:
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)
}
}
}
- 只处理数值类型,并对其他类型返回一个合理的错误:
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)
}
}
}
还有第三种可能性:为整个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) } } }
-
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) } } }
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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论