Search code examples
jsongotypesunmarshalling

Unmarshalling Dynamic JSON Data With Overlapping Fields in Golang


Sorry If i'm posting a question that has already been answered, but I can't seem to find any similar situations on here. I have a websocket client that receives dynamic json data with overlapping fields. The fact that the fields overlap has has made Unmarshalling very difficult for me.

I have structs for the data types I receive, but I need a way to check the json data before I unmarshal it to a specific struct. I was hoping that an interface could act as a temporary holder and I would then be able to match the interface to the specific struct I want to unmarshal to, but that doesn't seem possible, or I just don't know how to go about it. Here are a few examples of the data types I'm receiving and structs to go along with it in case that helps.

response 1: {"connectionID":17973829270596587247,"event":"systemStatus","status":"online","version":"1.9.0"}
response 2: {"channelID":328,"channelName":"ohlc-5","event":"subscriptionStatus","pair":"XBT/USD","status":"subscribed","subscription":{"interval":5,"name":"ohlc"}}
response 3: [328,["1649576721.042916","1649577000.000000","42641.50000","42641.50000","42641.50000","42641.50000","42641.50000","0.00335101",2],"ohlc-5","XBT/USD"]
response 4: {"event":"heartbeat"}

structs below
import (
    "time"
    "encoding/json"
)

type ConnStatus struct {
    ConnectionID        uint64          `json:"connectionID"`
    Event               string          `json:"event"`
    Status              string          `json:"status"`
    Version             string          `json:"version"`
}

type HeartBeat struct {
    Event               string          `json:"event"`
}

type OHLCsuccess struct {
    ChannelID           int             `json:"channelID"`
    ChannelName         string          `json:"channelName"`
    Event               string          `json:"event"`
    Pair                string          `json:"pair"`
    Status              string          `json:"status"`
    Subscription        OHLC            `json:"subscription"`
}

type OHLC struct {
    Interval        int         `json:"interval"`
    Name            string      `json:"name"`
}

type OHLCUpdates struct {
    ChannelID           int
    OHLCArray           OHLCNewTrade
    ChannelName         string
    Pair                string
}

type OHLCNewTrade struct {
    StartTime           UnixTime
    EndTime             UnixTime
    Open                float64
    High                float64
    Low                 float64
    Close               float64
    VWAP                float64
    Volume              float64
    Count               int
}

type UnixTime struct {
    time.Time
}

func (u *UnixTime) UnmarshalJSON(d []byte) error {
    var ts int64
    err := json.Unmarshal(d, &ts)
    if err != nil {
        return err
    }
    u.Time = time.Unix(ts, 0).UTC()
    return nil
}

Any idea(s) on how to go about this? Thanks in advance for the help!


Solution

  • Are you in control of the different responses? If so, wow about adding a "type" field to the top level?

    See "How to put everything at the top level" section on https://eagain.net/articles/go-dynamic-json/ for more info.

    E.g. (untested):

    func UnmarshalJSON(d []byte) error {
        var jsonValue map[string]interface{}
        err := json.Unmarshal(d, &jsonValue)
    
        if err != nil {
            return err
        }
    
        switch jsonValue["type"] {
        case 1:
            // unmarshal into struct type 1
        case 2:
            // unmarshal into struct type 2
        default:
            // throw err
        }
    
        // or if you don't have access to type:
        if jsonValue["connectionID"] != nil {
            // unmarshal into struct type 1
        }
    
        return nil
    }
    

    Alternatively you could try to (strictly) unmarshal into each struct, until you don't get an error, e.g. something like:

    func DetermineStruct(d []byte) int {
        var connStatus *ConnStatus
    
        reader := bytes.NewReader(d)
        decoder := json.NewDecoder(reader)
        decoder.DisallowUnknownFields()
    
        err := decoder.Decode(connStatus)
        if err == nil {
            panic(err)
        }
    
        err = json.Unmarshal(d, &connStatus)
        if err == nil {
            return 1
        }
    
        var ohlcSuccess OHLCsuccess
        err = json.Unmarshal(d, &ohlcSuccess)
        if err == nil {
            return 2
        }
    }