Существует несколько подходов к решению этой проблемы.
Самым простым для понимания идеи является использование факта, что 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
типа, и там катится так:
Разберите исходный объект в переменную типа map[string]interface{}
.
Переберите получившуюся карту и разберитесь с его элементы, основанные на их именах и их динамическом 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
имеют совместимые представления памяти (с возможностью преобразования типов друг в друга, попарно), поэтому Нужно использовать помощник присваивания, который бы преобразовывал каждое поле по отдельности или литерал структуры, как в примере.