How to parse JSON in golang without unmarshaling twice

前端 未结 3 1409
甜味超标
甜味超标 2021-01-06 17:16

I\'ve got a web socket connection that sends different types of messages in a JSON object, and I want to unmarshal the contents into some known structs. To do this, I figur

相关标签:
3条回答
  • 2021-01-06 17:40

    You will want to create a struct which contains all keys you might receive, then unmarshal once, finally based on which keys are nil you can decide what kind of packet you got.

    The example below displays this behavior: https://play.golang.org/p/aFG6M0SPJs

    package main
    
    import (
      "encoding/json"
      "fmt"
      "strings"
    )
    
    
    type Ack struct {
      Messages []string `json:"messages"`
    }
    
    type Packet struct {
      Ack  * Ack    `json:"ack"`
      Ping * string `json:"ping"`
    }
    
    func runTest(testJSON []byte) {
      var p = Packet{}
      err := json.Unmarshal(testJSON, &p)
      if err != nil {
        fmt.Println("error unmarshalling: ", err)
      }
      if (p.Ack != nil) {
        fmt.Println("Got ACK: ", strings.Join(p.Ack.Messages, ", "));
      } else if (p.Ping != nil){
              fmt.Println("Got PING");
      }
    }
    
    func main() {
      tests := [][]byte{
        []byte(`{"ack":{"messages":["Hi there","Hi again"]}}`),
        []byte(`{"ping": "Are you there?"}`),
      }
      for _, test := range tests {
        runTest(test)
      }
    
    }
    
    0 讨论(0)
  • 2021-01-06 17:53

    One solution is to partially unmarshal the data by unmarshalling the values into a json.RawMessage instead of an interface{}

    var myMap map[string]json.RawMessage
    

    Later in the switch, which still is required, you do not need to marshal. Just do:

    err = json.Unmarshal(v, &myAck)
    

    Playground: https://play.golang.org/p/NHd3uH5e7z

    0 讨论(0)
  • 2021-01-06 17:56

    Another solution is to use a common key between all messages (eg: type) which indicates what type of message it is. After this, if the messages aren't sufficiently complicated, you can build the raw structs from the other map keys / type assert map values:

    From some related websocket code I wrote that does this: https://github.com/blaskovicz/cut-me-some-slack/blob/master/chat/message.go#L66

    func DecodeClientMessage(c *ClientMessage) (typedMessage interface{}, err error) {
        buff := map[string]string{}
        err = json.NewDecoder(bytes.NewReader(c.Raw)).Decode(&buff)
        if err != nil {
            return
        }
    
        switch t := buff["type"]; t {
        case "message":
            channelID := buff["channel_id"]
            if channelID == "" {
                err = fmt.Errorf("invalid client message received: missing channel_id")
                return
            }
            cms := &ClientMessageSend{ChannelID: channelID, Text: buff["text"]}
            if cms.Text == "" {
                err = fmt.Errorf("invalid client message received: missing text")
            } else {
                typedMessage = cms
            }
    

    ... and then the caller code does a result.(type) assertion and switch: https://github.com/blaskovicz/cut-me-some-slack/blob/master/chat/hub.go#L150

    switch m := raw.(type) {
    case *ClientMessageHistory:
        channelID := h.resolveSlackChannel(m.ChannelID)
        if channelID == "" {
            log.Printf("error: no channel found matching %s\n", m.ChannelID)
            return
        }
        var username string
        if c.Client.User != nil {
            username = c.Client.User.Username
        } else {
            username = "<anonymous>"
        }
        log.Printf("sending previous messages for channel %s to client %s\n", channelID, username)
        for _, prevMessage := range h.previousMessages(channelID, m.Limit) {
            c.Client.send <- prevMessage
        }
    
    0 讨论(0)
提交回复
热议问题