gostructunmarshallinggo-interfacekraken.com

Cast a json in a properly struct instead of use an interface


i'm struggling to create a data structure for unmarshal the following json:

{
    "asks": [
        ["2.049720", "183.556", 1576323009],
        ["2.049750", "555.125", 1576323009],
        ["2.049760", "393.580", 1576323008],
        ["2.049980", "206.514", 1576322995]
    ],
    "bids": [
        ["2.043800", "20.691", 1576322350],
        ["2.039080", "755.396", 1576323007],
        ["2.036960", "214.621", 1576323006],
        ["2.036930", "700.792", 1576322987]
    ]
}

If I use the following struct with interfaces, there is no problem:

type OrderBook struct {
    Asks [][]interface{} `json:"asks"`
    Bids [][]interface{} `json:"bids"`
}

But i need a more strict typing, so i've tried with:

type BitfinexOrderBook struct {
    Pair string            `json:"pair"`
    Asks [][]BitfinexOrder `json:"asks"`
    Bids [][]BitfinexOrder `json:"bids"`
}

type BitfinexOrder struct {
    Price     string
    Volume    string
    Timestamp time.Time
}

But unfortunately i had not success.

This is the code that I have used for parse the Kraken API for retrieve the order book:

// loadKrakenOrderBook is delegated to load the data related to pairs info
func loadKrakenOrderBook(data []byte) (datastructure.BitfinexOrderBook, error) {
    var err error

    // Creating the maps for the JSON data
    m := map[string]interface{}{}
    var orderbook datastructure.BitfinexOrderBook

    // Parsing/Unmarshalling JSON
    err = json.Unmarshal(data, &m)

    if err != nil {
        zap.S().Debugw("Error unmarshalling data: " + err.Error())
        return orderbook, err
    }

    a := reflect.ValueOf(m["result"])

    if a.Kind() == reflect.Map {
        key := a.MapKeys()[0]
        log.Println("KEY: ", key)
        strct := a.MapIndex(key)
        log.Println("MAP: ", strct)
        m, _ := strct.Interface().(map[string]interface{})
        log.Println("M: ", m)
        data, err := json.Marshal(m)
        if err != nil {
            zap.S().Warnw("Panic on key: ", key.String(), " ERR: "+err.Error())
            return orderbook, err
        }
        log.Println("DATA: ", string(data))
        err = json.Unmarshal(data, &orderbook)
        if err != nil {
            zap.S().Warnw("Panic on key: ", key.String(), " during unmarshal. ERR: "+err.Error())
            return orderbook, err
        }
        return orderbook, nil

    }
    return orderbook, errors.New("UNABLE_PARSE_VALUE")
}

The data that i use for test are the following:

{
    "error": [],
    "result": {
        "LINKUSD": {
            "asks": [
                ["2.049720", "183.556", 1576323009],
                ["2.049750", "555.125", 1576323009],
                ["2.049760", "393.580", 1576323008],
                ["2.049980", "206.514", 1576322995]
            ],
            "bids": [
                ["2.043800", "20.691", 1576322350],
                ["2.039080", "755.396", 1576323007],
                ["2.036960", "214.621", 1576323006],
                ["2.036930", "700.792", 1576322987]
            ]
        }
    }
}

EDIT

NOTE: the data that i receive in input is the latest json that i've post, not the array of bids and asks.

I've tried to integrate the solution proposed by @chmike. Unfortunately there is a few preprocessing to be made, cause the data is the latest json that i've post.

So i've changed to code as following in order to extract the json data related to asks and bids.

func order(data []byte) (datastructure.BitfinexOrderBook, error) {
    var err error

    // Creating the maps for the JSON data
    m := map[string]interface{}{}
    var orderbook datastructure.BitfinexOrderBook
    // var asks datastructure.BitfinexOrder
    // var bids datastructure.BitfinexOrder
    // Parsing/Unmarshalling JSON
    err = json.Unmarshal(data, &m)

    if err != nil {
        zap.S().Warn("Error unmarshalling data: " + err.Error())
        return orderbook, err
    }

    // Extract the "result" json
    a := reflect.ValueOf(m["result"])

    if a.Kind() == reflect.Map {
        key := a.MapKeys()[0]
        log.Println("KEY: ", key)
        log.Println()
        strct := a.MapIndex(key)
        log.Println("MAP: ", strct)
        m, _ := strct.Interface().(map[string]interface{})
        log.Println("M: ", m)
        log.Println("Asks: ", m["asks"])
        log.Println("Bids: ", m["bids"])

        // Here i retrieve the asks array
        asks_data, err := json.Marshal(m["asks"])
        log.Println("OK: ", err)
        log.Println("ASKS: ", string(asks_data))
        var asks datastructure.BitfinexOrder
        // here i try to unmarshal the data into the struct
        asks, err = UnmarshalJSON(asks_data)
        log.Println(err)
        log.Println(asks)

    }
    return orderbook, errors.New("UNABLE_PARSE_VALUE")
}

Unfortunately, i receive the following error:

json: cannot unmarshal array into Go value of type json.Number


Solution

  • As suggested by @Flimzy, you need a custom Unmarshaler. Here it is.

    Note that the BitfinexOrderBook definition is slightly different from yours. There was an error in it.

    // BitfinexOrderBook is a book of orders.
    type BitfinexOrderBook struct {
        Asks []BitfinexOrder `json:"asks"`
        Bids []BitfinexOrder `json:"bids"`
    }
    
    // BitfinexOrder is a bitfinex order.
    type BitfinexOrder struct {
        Price     string
        Volume    string
        Timestamp time.Time
    }
    
    // UnmarshalJSON decode a BifinexOrder.
    func (b *BitfinexOrder) UnmarshalJSON(data []byte) error {
        var packedData []json.Number
        err := json.Unmarshal(data, &packedData)
        if err != nil {
            return err
        }
        b.Price = packedData[0].String()
        b.Volume = packedData[1].String()
        t, err := packedData[2].Int64()
        if err != nil {
            return err
        }
        b.Timestamp = time.Unix(t, 0)
        return nil
    }
    

    Note also that this custom unmarshaler function allows you to convert the price or volume to a float, which is probably what you want.