Как размаршализировать JSON пользовательским способом в Golang

Когда я впервые работал над библиотекой gocoinex, мне потребовалась некоторая пользовательская настройка процесса размаршалинга JSON, и я хочу поделиться с вами своими наработками здесь, но перед этим мы сделаем обзор того, как мы можем размаршалить JSON в golang.

Это примеры из реального мира. (API биржи coinex)

Сначала мы делаем запрос следующим образом:

raw_response, _ := http.Get("https://api.coinex.com/v1/market/list")
Войти в полноэкранный режим Выйти из полноэкранного режима

И получим json-объект следующего вида:

{
    "code": 0,
    "data": [
        "LTCBCH",
        "ETHBCH",
        "ZECBCH",
        "DASHBCH"
    ],
    "message": "Ok"
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Но мы должны разобрать его,
Есть разные способы разобрать его,
Вы можете использовать функции NewDecoder или Unmarshal из пакета json,
и вы можете декодировать его в struct или в map[string]interface{}.

Это зависит от ваших предпочтений, но в данном случае я предпочитаю комбинацию NewDecoder и struct.

Итак, мы должны создать структуру, подобную этой:

type AllMarketList struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
    Data    []string
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Также мы можем иметь встроенные структуры, например, разобьем последнюю структуру на две:

type GeneralResponse struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
}

type AllMarketList struct {
    GeneralResponse
    Data    []string
}
Вход в полноэкранный режим Выход из полноэкранного режима

И нет никакой разницы.

Наконец, используйте NewDecoder для декодирования raw_response в структуру AllMarketList:

var allMarketList AllMarketList
json.NewDecoder(raw_response.Body).Decode(&allMarketList)
Вход в полноэкранный режим Выход из полноэкранного режима

Завершенный код

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
)

type AllMarketList struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
    Data    []string
}

func main() {
    raw_response, _ := http.Get("https://api.coinex.com/v1/market/list")

    var allMarketList AllMarketList
    if err := json.NewDecoder(raw_response.Body).Decode(&allMarketList); err != nil {
        fmt.Println(err)
    }
    defer raw_response.Body.Close()
    fmt.Printf("%+vn", allMarketList)
}
Вход в полноэкранный режим Выход из полноэкранного режима

Пример второй

Представьте, что у нас есть json следующего вида:

{
  "code": 0,
  "data": {
    "date": 1513865441609, # server time when returning
    "ticker": {
        "open": "10", # highest price
        "last": "10.00", # latest price 
        "vol": "110" # 24H volume
    }
  },
  "message" : "Ok"
}
Вход в полноэкранный режим Выход из полноэкранного режима

Мы собираемся улучшить некоторые вещи в процессе декодирования.

  1. В данном случае временная метка Unix не может быть разобрана, мы должны предоставить ей возможность.
  2. Мы хотим удалить ключ «ticker» и получить доступ к «open», «last», «vol» непосредственно из «data».
  3. «last» должен быть экспортирован в поле с именем «Close»! (то же самое происходит с «vol» и «Volume».
  4. «open», «last», «vol» должны быть float, а не string, но мы оставим их для следующего примера.

Проблемы 1 и 2 могут быть решены путем реализации метода UnmarshalJSON на любой структуре, которую мы хотим декодировать.
Проблема 3 будет легко решена с помощью тегов json. (Я упомянул об этом в коде ниже).

Наша конечная структура должна выглядеть следующим образом:

// Final struct
type SingleMarketStatistics struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
    Data    TickerData
}

// Inner struct that we should implement to solve problem 2
type TickerData struct {
    ServerTime CTime   `json:"date"` // CTime is short for CustomTime
    Open       float64 `json:"open"`
    Close      float64 `json:"last"` // Different Attribute name and tag name
    Volume     float64 `json:"vol"` // Different Attribute name and tag name
}

// Custome time
// Inner struct that we should implement to solve problem 1
type CTime struct {
    time.Time
}
Вход в полноэкранный режим Выход из полноэкранного режима

Реализация пользовательского времени

func (t *CTime) UnmarshalJSON(data []byte) error {
    // Ignore null, like in the main JSON package.
    if string(data) == "null" || string(data) == `""` {
        return nil
    }
    // Fractional seconds are handled implicitly by Parse.
    i, err := strconv.ParseInt(string(data), 10, 64)
    update := time.UnixMilli(i)
    *t = CTime{update}
    return err
}
Вход в полноэкранный режим Выход из полноэкранного режима

И мы больше не получаем ошибку!
Этот метод будет автоматически использоваться (благодаря интерфейсам!) всякий раз, когда мы захотим декодировать время в CTime!

Реализация пользовательских данных

func (t *TickerData) UnmarshalJSON(data []byte) error {
    if string(data) == "null" || string(data) == `""` {
        return nil
    }

    // This is how this json really looks like.
    var realTicker struct {
        ServerTime CTime `json:"date"`
        Ticker     struct {
            // tags also can be omitted when we're using UnmarshalJSON.
            Open   string `json:"open"`
            Close  string `json:"last"`
            Volume string `json:"vol"`
        } `json:"ticker"`
    }

    // Unmarshal the json into the realTicker struct.
    if err := json.Unmarshal(data, &realTicker); err != nil {
        return err
    }

    // Set the fields to the new struct,
    // with any shape it has,
    // or anyhow you want.
    *t = TickerData{
        ServerTime: realTicker.ServerTime,
        Open:       realTicker.Ticker.Open,
        Close:      realTicker.Ticker.Close,
        Volume:     realTicker.Ticker.Volume,
    }

    return nil
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Теперь просто используйте NewDecoder, как и раньше, никаких изменений не требуется.

var singleMarketStatistics SingleMarketStatistics 
json.NewDecoder(raw_response.Body).Decode(&allMarketList)
Вход в полноэкранный режим Выход из полноэкранного режима

Пример третий

Представьте себе JSON следующего вида:

{
  "asks": [ // This is a array of asks
    [ // This is a array of ONE ask
      "10.00", // Price of ONE ask
      "0.999", // Amount of ONE ask
    ]
  ],
  "bids": [ // Same structure as asks
    [
      "10.00",
      "1.000",
    ]
  ]
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Как совершенно ясно, непрофессиональным способом, мы должны декодировать «asks» в [][]string и получить доступ к цене первого запроса следующим образом asks[0][0] и сумме asks[0][1].
Кто помнит, что 0 — это цена, а 1 — сумма? Что из этого было? 😄
Поэтому мы будем управлять ими в методе UnmarshalJSON.
Также мы решим проблему 4 из предыдущего примера, которая существует и здесь.

type BidAsk struct {
    // Tags are not needed.
    Price  float64 `json:"price"`  // Bid or Ask price
    Amount float64 `json:"amount"` // Bid or Ask amount
}

func (t *BidAsk) UnmarshalJSON(data []byte) error {
    // Ignore null, like in the main JSON package.
    if string(data) == "null" || string(data) == `""` {
        return nil
    }

    // Unmarshal to real type.
    var bisask []string
    if err := json.Unmarshal(data, &bisask); err != nil {
        return err
    }

    // Change value type from string to float64.
    price, err := strconv.ParseFloat(bisask[0], 64)
    if err != nil {
        return err
    }
    amount, err := strconv.ParseFloat(bisask[1], 64)
    if err != nil {
        return err
    }

    // Map old structure to new structure.
    *t = BidAsk{
        Price:  price,
        Amount: amount,
    }
    return err
}

type MarketDepth struct {
    Asks   []BidAsk `json:"asks"` // Ask depth
    Bids   []BidAsk `json:"bids"` // Bid depth
}
Вход в полноэкранный режим Выход из полноэкранного режима

Опять же, мы просто используем:

var marketDepth MarketDepth 
json.NewDecoder(raw_response.Body).Decode(&marketDepth)
Войти в полноэкранный режим Выйти из полноэкранного режима

И наслаждаемся красотой результата:

for i, ask := range data.Data.Asks {
    fmt.Printf("Ask %vn", i)
    fmt.Printf("  Price: %vn", ask.Price) // here is the beauty
    fmt.Printf("  Amount: %vn", ask.Amount) // here is the beauty
    fmt.Println()
}
for i, bid := range data.Data.Bids {
    fmt.Printf("Bid %vn", i)
    fmt.Printf("  Price: %vn", bid.Price) // here is the beauty
    fmt.Printf("  Amount: %vn", bid.Amount) // here is the beauty
    fmt.Println()
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Оцените статью
devanswers.ru
Добавить комментарий