api - 在 Go 中序列化 API 响应
问题描述
作为 Go 新手,我还没有找到解决问题的方法。我正在使用一个给出不一致响应的 API。以下是 API 给出的两个示例响应:
{
"key_a": "0,12",
"key_b": "0,1425",
"key_c": 9946
}
和
{
"key_a": 3.65,
"key_b": 3.67,
"key_c": 2800
}
我面临的问题是,在我的数据类型中,我无法处理模棱两可的数据类型。这是我的数据类型:
type apiResponse struct {
Key_a float64 `json:"key_a"`
Key_b float64 `json:"key_b"`
Key_c int `json:"key_c"`
}
这是调用 API 的代码的简化版本:
func callAPI() (apiResponse, error) {
var a apiResponse
req, err := http.NewRequest("GET", "https://www.apiurl.com", nil)
client := &http.Client{}
resp, err := client.Do(req)
data, err := ioutil.ReadAll(resp.Body)
json.Unmarshal(data, &a)
return a, err
}
如何处理 API 响应中更改的数据类型,以确保我可以在其余代码中使用这些值?
解决方案
有多种方法可以解决这个问题。
理解这个想法的最简单的方法是利用encoding/json
解组器检查接收变量的类型是否实现了encoding/json.Unmarshaler
接口,如果实现了,它调用该类型的UnmarshalJSON
方法,将原始数据传递给它,否则它会尝试解释自己。该方法负责采用它喜欢的任何方法将源原始字节解释为 JSON 文档并填充调用它的变量。
我们可以利用它来尝试查看原始输入数据是否以"
字节开头(因此它是一个字符串)或不是(因此它应该是一个浮点数)。
为此,我们将创建一个自定义类型kinkyFloat
,实现encoding/json.Unmarshaler
接口:
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
)
type apiResponse struct {
Key_a kinkyFloat `json:"key_a"`
Key_b kinkyFloat `json:"key_b"`
Key_c int `json:"key_c"`
}
type kinkyFloat float64
func (kf *kinkyFloat) UnmarshalJSON(b []byte) error {
if len(b) == 0 {
return errors.New("empty input")
}
if b[0] == '"' {
// Get the data between the leading and trailing " bytes:
b = b[1 : len(b)-1]
if i := bytes.IndexByte(b, ','); i >= 0 {
b[i] = '.'
}
}
// At this point, we have b containing a set of bytes without
// encolsing "-s and with the decimal point presented by a dot.
var f float64
if err := json.Unmarshal(b, &f); err != nil {
return err
}
*kf = kinkyFloat(f)
return nil
}
func main() {
for _, input := range []string{
`{"Key_a": "0,12", "Key_b": "12,34", "Key_c": 42}`,
`{"Key_a": 0.12, "Key_b": 12.34, "Key_c": 42}`,
} {
var resp apiResponse
err := json.Unmarshal([]byte(input), &resp)
if err != nil {
fmt.Println("error: ", err)
continue
}
fmt.Println("OK: ", resp)
}
}
如你所见,解组方法检查传递给它的原始数据是否以"
字节开头,如果是,它首先去掉封闭的双引号,然后将所有,
-s替换为.
-s — 这样更新后的原始数据看起来就像一个正确的 JSON 格式的浮点数。
如果原始数据不以双引号开头,则不会以任何方式触及它。
毕竟,我们调用了encoding/json
自己的解组代码——告诉它再次解组我们的字节块;请注意有关此调用的两件事:
- 我们知道数据被格式化为一个正确序列化的浮点数:要么它已经看起来像这样,要么我们已经修复了它。
- 我们确保将类型的变量传递给它
float64
,而不是kinkyFloat
- 否则我们最终会递归调用自定义解组方法,最终导致堆栈溢出。
这种方法的一个警告是,结果结构的字段是 type kinkyFloat
,而不是 plain float64
,这可能导致需要在代码中到处溢出类型转换,以便在算术表达式中使用它们。
如果这不方便,还有其他方法可以解决问题。
通常的方法是定义UnmarshalJSON
目标struct
类型本身,并且滚动如下:
将源对象解组为 类型的变量
map[string]interface{}
。遍历生成的映射并根据其名称和动态未编组类型处理其元素,这将取决于 JSON 解析器真正看到的内容;像这样的东西:
var resp apiResponse for k, v := range resultingMap { var err error switch k { case "Key_a": resp.Key_a, err = toFloat64(v) case "Key_b": resp.Key_b, err = toFloat64(v) case "Key_c": resp.Key_c = v.(int) } if err != nil { return err } }
… where
toFloat64
定义如下:func toFloat64(input interface{}) (float64, error) { switch v := input.(type) { case float64: return v, nil case string: var f float64 // parse the string as in the code above. return f, nil default: return 0, fmt.Errorf("invalid type: %T", input) } }
另一种方法是有一对用于解组的结构:一个看起来像
type apiResponse struct {
Key_a float64
Key_b float64
Key_c int
}
另一个专门用于解组:
type apiRespHelper struct {
Key_a kinkyFloat
Key_b kinkyFloat
Key_c int
}
UnmarshalJSON
然后,您可以定义apiResponse
which 可以像这样滚动:
func (ar *apiResponse) UnmarshalJSON(b []byte) error {
var raw apiRespHelper
if err := json.Unmarshal(b, &raw); err != nil {
return err
}
*ar = apiResponse{
Key_a: float64(raw.Key_a),
Key_b: float64(raw.Key_b),
Key_c: raw.Key_c,
}
return nil
}
由于这两种类型都具有其字段类型的兼容内存表示,因此可以进行简单的类型转换。
更新:不幸的是,即使这两种类型的字段具有兼容的内存表示(可以相互类型转换,成对),简单的转换(如 in)*ar = apiResponse(raw)
也不起作用struct
,因此必须使用一个赋值助手来对每个类型进行类型转换单独的字段或示例中的结构文字。
推荐阅读
- java - Thymeleaf 表单验证
- javascript - 如何在窗口环境中获取我的 JavaScript UMD 包的完整字符串?`.toString()` 不起作用
- amazon-web-services - 通过 API Gateway 公开在 EKS 中运行的 Kubernetes 服务
- json - 如何在 Swift 中使用 Codable 解析 JSON?
- css - Safari 忽略 CSS :not() 规则
- azure - 我可以将 Azure 交互模式用于 azure-cli-ml 扩展吗?
- ios - Object-c 在变量的值改变后运行代码,怎么办?
- java - 编码毕达哥拉斯定理
- swift - 如何在 Kotlin 中扩展 Activity,例如在 Swift 中扩展 UIViewController
- gradle - Gitlab CI 缓存更新