在 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— 否则我们最终会递归调用自定义解编方法,最终导致堆栈溢出。

这种方法的一个警告是,结果结构的字段是kinkyFloat类型,而不是普通float64,这可能会导致需要在代码中到处溢出类型转换,以便在算术表达式中使用它们。

如果这不方便,还有其他方法可以解决问题。

一种常用的方法是在目标struct类型本身上定义UnmarshalJSON,如下所示:

  1. 将源对象取消封送为类型map[string]interface{}的变量。

  2. 迭代生成的映射,并根据其名称和动态未编组类型处理其元素,这将取决于 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
    }
    }
    

    。其中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
}

然后,您可以在apiResponse上定义可以像这样滚动的UnmarshalJSON

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
}

由于这两种类型都具有其字段类型的兼容内存表示形式,因此简单的类型转换是有效的。
更新:不幸的是,简单的转换(如在*ar = apiResponse(raw)中(不起作用,即使两种struct类型的字段具有兼容的内存表示形式(可以相互类型转换,成对(,因此必须使用赋值帮助程序,该帮助程序将单独对每个字段进行类型转换,或者像示例中那样对结构文字进行类型转换。

最新更新