首页 > 解决方案 > 在 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 响应中更改的数据类型,以确保我可以在其余代码中使用这些值?

标签: apigoserialization

解决方案


有多种方法可以解决这个问题。

理解这个想法的最简单的方法是利用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类型本身,并且滚动如下:

  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
        }
    }
    

    … wheretoFloat64定义如下:

    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然后,您可以定义apiResponsewhich 可以像这样滚动:

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,因此必须使用一个赋值助手来对每个类型进行类型转换单独的字段或示例中的结构文字。


推荐阅读