Go, SQS and Unmarshal
Making sense of what our client sends us, we use json.Unmarshal
Let's research a use case where we don't know about data type beforehand. Most people use interface{}
but is it optimal? Read on to know.
// Envelope represents basic sqs event format
type Envelope struct {
EventID string `json:"eventId"`
EventTime time.Time `json:"eventTime"`
Source string `json:"source"` // service name that publishes the message
Type string `json:"type"` // for example "UPSERT"
Data interface{} `json:"data"` // populate with your message
}
Example:
package main
import (
"fmt"
"encoding/json"
"time"
"github.com/satori/go.uuid"
)
type Envelope struct {
EventID string `json:"eventId"`
EventTime time.Time `json:"eventTime"`
Source string `json:"source"`
Type string `json:"type"`
Data interface{} `json:"data"` // populate with your message
}
type Payload struct {
Email string
ID uint64
}
func main() {
e := Envelope{
EventID: uuid.NewV4().String(),
EventTime: time.Now(),
Source: "GO_PLAYGROUND",
Type: "TEST",
Data: Payload{
Email: "lorem.ipsum@lipsum.com",
ID: 123456,
},
}
data, _ := json.Marshal(e)
var incoming Envelope
_ = json.Unmarshal(data, &incoming)
fmt.Println("Hello, playground", incoming.Data)
}
Output:
Hello, playground map[Email:lorem.ipsum@lipsum.com ID:123456]
With Data as interface{}, on unmarshall
it will lead to a map data structure(map[string]interface{}
). It's really hard to extract payload data from the above construct.
It's better to parse it this way
package main
import (
"fmt"
"reflect"
"encoding/json"
"time"
"github.com/satori/go.uuid"
)
type Envelope struct {
EventID string `json:"eventId"`
EventTime time.Time `json:"eventTime"`
Source string `json:"source"`
Type string `json:"type"`
Data interface{} `json:"data"` // populate with your message
}
type Payload struct {
Email string
ID uint64
}
func main() {
e := Envelope{
EventID: uuid.NewV4().String(),
EventTime: time.Now(),
Source: "GO_PLAYGROUND",
Type: "TEST",
Data: Payload{
Email: "lorem.ipsum@lipsum.com",
ID: 123456,
},
}
data, _ := json.Marshal(e)
incoming := Envelope{
Data:&Payload{},
}
_ = json.Unmarshal(data, &incoming)
fmt.Println("Hello, playground", incoming.Data)
fmt.Println("Payload type", reflect.TypeOf(incoming.Data))
}
Output:
Hello, playground &{lorem.ipsum@lipsum.com 123456}
Payload type *main.Payload
Looks like we got rid of map
and got Payload concrete type instead, which is cool but did you notice that we are required to know the concrete type beforehand !
Enters json.RawMessage...
// Envelope represents basic sqs event format
type Envelope struct {
EventID string `json:"eventId"`
EventTime time.Time `json:"eventTime"`
Source string `json:"source"` // service name that publishes the message
Type string `json:"type"` // for example "UPSERT"
Data json.RawMessage `json:"data"` // populate with your message
}
With Data as json.RawMessage, on umarshall
it will lead to byte slice(json.RawMessage
).
Example:
package main
import (
"encoding/json"
"fmt"
"time"
"github.com/satori/go.uuid"
)
type Envelope struct {
EventID string `json:"eventId"`
EventTime time.Time `json:"eventTime"`
Source string `json:"source"` // service name that publishes the message
Type string `json:"type"` // for example "UPSERT"
Data json.RawMessage `json:"data"` // populate with your message
}
type Payload struct {
Email string
ID uint64
}
func main() {
bytepayload, _ := json.Marshal(
&Payload{
Email: "lorem.ipsum@lipsum.com",
ID: 123456,
},
)
rawbytepayload := json.RawMessage(bytepayload)
e := Envelope{
EventID: uuid.NewV4().String(),
EventTime: time.Now(),
Source: "GO_PLAYGROUND",
Type: "TEST",
Data: rawbytepayload,
}
data, _ := json.Marshal(e)
var incoming Envelope
_ = json.Unmarshal(data, &incoming)
fmt.Println("Hello, playground", incoming.Data)
}
Output:
Hello, playground [123 34 69 109 97 105 108 34 ........]
As json.RawMessage
creates a byte slice at the end so it basically can consume anything.
package main
import (
"fmt"
"reflect"
"encoding/json"
"time"
"github.com/satori/go.uuid"
)
// Envelope is envelope at our sqs publisher
type Envelope struct {
EventID string `json:"eventId"`
EventTime time.Time `json:"eventTime"`
Source string `json:"source"`
Type string `json:"type"`
Data interface{} `json:"data"` // populate with your message
}
// IncomingEnvelope is envelope at our generic sqs consumer.
type IncomingEnvelope struct {
EventID string `json:"eventId"`
EventTime time.Time `json:"eventTime"`
Source string `json:"source"`
Type string `json:"type"`
Data json.RawMessage `json:"data"` // populate with your message
}
type Payload struct {
Email string
ID uint64
}
func main() {
e := Envelope{
EventID: uuid.NewV4().String(),
EventTime: time.Now(),
Source: "GO_PLAYGROUND",
Type: "TEST",
Data: Payload{
Email: "lorem.ipsum@lipsum.com",
ID: 123456,
},
}
data, _ := json.Marshal(e)
var incoming IncomingEnvelope // this is diff
_ = json.Unmarshal(data, &incoming)
fmt.Println("Hello, playground", incoming.Data)
fmt.Println("Payload type", reflect.TypeOf(incoming.Data))
}
Output:
Hello, playground [123 34 ......]
Payload type json.RawMessage
Nice! We can then use the data slice and parse it into any valid concrete type.
So using interface{}
at consumer side doesn't work? It does!
package main
import (
"encoding/json"
"fmt"
"time"
"github.com/satori/go.uuid"
)
type Envelope struct {
EventID string `json:"eventId"`
EventTime time.Time `json:"eventTime"`
Source string `json:"source"` // service name that publishes the message
Type string `json:"type"` // for example "UPSERT"
Data json.RawMessage `json:"data"` // populate with your message
}
type IncomingEnvelope struct {
EventID string `json:"eventId"`
EventTime time.Time `json:"eventTime"`
Source string `json:"source"` // service name that publishes the message
Type string `json:"type"` // for example "UPSERT"
Data interface{} `json:"data"` // populate with your message
}
type Payload struct {
Email string
ID uint64
}
func main() {
bytepayload, _ := json.Marshal(
&Payload{
Email: "lorem.ipsum@lipsum.com",
ID: 123456,
},
)
rawbytepayload := json.RawMessage(bytepayload)
e := Envelope{
EventID: uuid.NewV4().String(),
EventTime: time.Now(),
Source: "GO_PLAYGROUND",
Type: "TEST",
Data: rawbytepayload,
}
data, _ := json.Marshal(e)
var incoming = IncomingEnvelope{
Data:&Payload{}, // we need to know this beforehand
}
_ = json.Unmarshal(data, &incoming)
fmt.Println("Hello, playground", incoming.Data)
}
Output:
Hello, playground &{lorem.ipsum@lipsum.com 123456}
So using above shared solution on such a type can work for my consumer? No.
Why ? Being a generic consumer I don't know what type my consumer has sent when it published the event!
TLDR; Use json.RawMessage
where you are not sure about exact concrete types. In my use-case it was a generic SQS subscriber to be used by multiple backend services/workers.
Bonus: It outputs byte slice which more efficient to move around(data is still kept low level) rather than a map. You can read about this more on Googling.
All examples presented above are go-playground runnable, do try them. I don't suggest to ignore errors in production code.
Thanks!