Сериализация ответа API в Go - PullRequest
1 голос
/ 26 января 2020

Будучи новичком в 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, чтобы убедиться, что я Можно использовать значения в остальной части моего кода?

1 Ответ

4 голосов
/ 26 января 2020

Существует несколько подходов к решению этой проблемы.

Самым простым для понимания идеи является использование факта, что encoding/json unmarshaler проверяет, реализует ли тип принимающей переменной encoding/json.Unmarshaler интерфейс, и если он это делает, он вызывает метод UnmarshalJSON этого типа, передавая ему необработанные данные, которые он иначе попытался бы интерпретировать сам. Этот метод отвечает за любой подход, который ему нравится, интерпретировать исходные необработанные байты как JSON документ и заполнять переменную, к которой он был вызван.

Мы можем использовать это, чтобы проверить, являются ли необработанные входные данные начинается с байта " (и поэтому это строка) или нет (и поэтому это, предположительно, число с плавающей точкой).

Для этого мы создадим пользовательский тип kinkyFloat, реализующий encoding/json.Unmarshaler interface:

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

Как видите, метод unmarshaling проверяет, начинается ли с него необработанные данные, начинающиеся с байта ", и, если это так, сначала удаляет заключающие двойные кавычки, а затем заменяет все , -s на . -s - так, чтобы обновленные необработанные данные выглядели как правильно JSON -форматированный float.

Если необработанные данные не начинаются с двойной кавычки, это никак не трогать.

В конце концов, мы сами вызываем код демаршалирования encoding/json и просим его еще раз демаршировать наш блок байтов; обратите внимание на две вещи об этом вызове:

  • Мы знаем, что данные отформатированы как правильно сериализованное число с плавающей запятой: либо оно уже выглядело как таковое, либо мы исправили его.
  • Мы делаем обязательно передайте ему переменную типа float64, а не kinkyFloat - иначе мы бы в конечном итоге рекурсивно вызвали пользовательский метод демаршалинга, что в конечном итоге привело к переполнению стека.

Предупреждение Этот подход заключается в том, что поля результирующей структуры имеют тип kinkyFloat, а не просто float64, что может привести к необходимости разливать преобразования типов здесь и там в коде, который должен использовать их в арифметике c выражения.

Если это неудобно, есть другие способы решения проблемы.

Обычный подход - определить UnmarshalJSON для самого целевого struct типа, и там катится так:

  1. Разберите исходный объект в переменную типа map[string]interface{}.

  2. Переберите получившуюся карту и разберитесь с его элементы, основанные на их именах и их динамическом c немаршализованном типе, который будет зависеть от того, что действительно видел парсер 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
}

Затем вы можете определить UnmarshalJSON на apiResponse, который может катиться так:

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 имеют совместимые представления памяти (с возможностью преобразования типов друг в друга, попарно), поэтому Нужно использовать помощник присваивания, который бы преобразовывал каждое поле по отдельности или литерал структуры, как в примере.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...