Defer Unmarshaling in Go

ยท

4 min read

In this blog, let us understand a unique problem of dynamic unmarshalling, where one might encounter it and how json.RawMessage helps us navigate it.

So, we are all much familiar with HTTP endpoints. The client's consumption logic is straightforward since you have a separate endpoint for separate resources.

When you are in WebSocket world, you would send and receive all sorts of different messages on the same WebSocket connection. And when you do this you often come across the unique problem of intelligently deserialising these messages. Because you might end up deserialising all the messages entirely every time !!! ๐Ÿคฏ

That's Ugly! ๐Ÿคข

Let's say as a WebSocket server we want to send 2 types of messages:

type MessageType_A struct {
    Name  string `json:"name"`
    Place string `json:"place"`
}

type MessageType_B struct {
    Animal string `json:"animal"`
    Thing  string `json:"thing"`
}

To send these messages you need to marshal these structs and write those bytes on the WebSocket connection. The code would look something like this:

websocket_server.go --- 1.0

messageA := MessageTypeA { Name:  "Pankhudi", Place: "India"}
// OR
messageB := MessageTypeB { Animal: "dog", Thing:  "cap"}

bytes, err := json.Marshal(messageA) // OR messageB 
err = websocketConn.WriteMessage(websocket.TextMessage, bytes)

Now, the WebSocket client's deserializing logic is not aware beforehand of what type of message it is going to receive. You would need to somehow know which struct to initialize and unmarshal the bytes into.

websocket_client.go --- 1.0

bytes, _, err := websocketConn.Read()
if err != nil {...}

messageA := MessageTypeA{}
    // OR
messageB := MessageTypeB{}

json.Unmarshal(dataBytes, &messageA)
    // OR
json.Unmarshal(dataBytes, &messageA)

How do I know which one to unmarshal it to ???

json.RawMessage to our rescue! ๐Ÿ˜Ž

On the server, let's create a wrapper struct and introduce an additional attribute called MessageType and the actual message as something generic like "content"

websocket_server.go

type MessageWrapper struct {
    MessageType   string `json:"message_type"`
    MessageType_A `json:"content"`
}

Create an instance and populate it. Marshal it and write it on the WebSocket connection:

websocket_server.go --- 2.0

messageA := MessageWrapper{ 
      MessageType:   "A",
      MessageType_A: MessageType_A{Name: "Pankhudi", Place: "India"}
    }

bytes, err := json.Marshal(messageA)

err = websocketConn.WriteMessage(websocket.TextMessage, bytes)
if err != nil {...}

On the client end use json.RawMessage

If you look inside the package it's essentially a type alias for a byte array. Because that's the most basic form to represent data! Refer to the library code: https://github.com/golang/go/blob/master/src/encoding/json/stream.go#L255

websocket_client.go --- 2.0

type MessageWrapper struct {
    MessageType string          `json:"message_type"`
    Content     json.RawMessage `json:"content"`
}

Now when you unmarshal the received bytes into the wrapper struct :

dataBytes, _, err := wsutil.ReadServerData(conn)
if err != nil {...}

messageWrapper := MessageWrapper{}
err = json.Unmarshal(dataBytes, &messageWrapper)
if err != nil {...}

By doing this, the unmarshaler populates only the MessageType field. You don't believe me ? ๐Ÿคจ ####Let's have a look at the demo: ๐Ÿ’ป

WebSocket Server:

websocket server starting

WebSocket Client:

websocket client starting

WebSocket Server received hit from the client and sent the message

server sent back the message of type A

On the client end, post unmarshalling - you get to see the message type but the actual is still bytes and raw!

type printed, but content is still printed as byte representation

And you can happily defer unmarshalling the rest of the message based on message type! ๐Ÿค—

unmarshalling to actual struct

So, In a nutshell when to use json.RawMessage ?

  1. When you want to ignore certain parts of your json

    • Why would I even do that? - you ask. Well, let's say in the above WebSocket connection, the server needed to send Keep-Alive packets. In that case, knowing only the message type should serve the purpose.
  2. When you want to take explicit control of unmarshalling the data and thus defer that until you've branched out based on the some condition

    • We saw our above use-case to match here.

####Refer the Github gists here : https://gist.github.com/PankhudiB/e06c56bcd65c329e6996b611d118f7ed

Hope this blog helped you understand the awesomeness of json.RawMessage ! Let me know in the comments if you have any questions or feedbacks! Happy Coding! ๐Ÿ‘ฉโ€๐Ÿ’ป

ย