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 figure I should do the following:
Step 1) Unmarshal the JSON into a generic map[string]interface{}
Step 2) Find the key I'm looking for
Step 3) Try to cast the value into one of my types (this fails)
Step 3 alternate) json marshal this value and unmarshal it to my known struct
If I try to use myStruct, ok := value.(myType)
it will fail, but if I json.marshal(value) and then json.unmarshal to myStruct, it works just fine. Is that how I'm supposed to do this? Going json-> map[string]interface{} -> json -> myStruct seems redundant to me.
Sample code:
https://play.golang.org/p/to_0Id_ja9
package main
import (
"encoding/json"
"fmt"
)
type Ping struct {
Ping string `json:"ping"`
}
type Ack struct {
Messages []Message `json:"messages"`
}
type Message string
func main() {
testJSON := []byte(`{"ack":{"messages":["Hi there","Hi again"]}}`)
var myAck = Ack{}
var myMap map[string]interface{}
err := json.Unmarshal(testJSON, &myMap)
if err != nil {
fmt.Println("error unmarshalling: ", err)
}
for k, v := range myMap {
fmt.Printf("key: %s, value: %s
", k, v)
switch k {
case "ping":
fmt.Println(k, " is a ping", v)
case "ack":
fmt.Println(k, " is an ack containing a message list")
ackjson, err := json.Marshal(v)
if err != nil {
fmt.Println("marshal error: ", err)
}
err = json.Unmarshal(ackjson, &myAck)
if err != nil {
fmt.Println("unmarshal error", err)
} else {
fmt.Println("New ack object: ", myAck)
}
default:
fmt.Printf("%s is of a type (%T) I don't know how to handle", k, v)
}
}
}
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)
}
}
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
", 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
", channelID, username)
for _, prevMessage := range h.previousMessages(channelID, m.Limit) {
c.Client.send <- prevMessage
}
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