JSON Parsing with Go Embedded Structs
Go has a feature called embedding that allows you to create higher level interfaces by combining existing interfaces. But embedding can also be applied to structs, which is very useful for JSON parsing/decoding.
Consider the common case of decoding JSON objects that have a timestamp field, in this case encoded as a unix integer timestamp, as JSON has no native date support (this same solution could be applied to a iso-8601 encoded date string).
Here's a PriceTick
json object and struct that will decode but isn't quite what we want.
{
"timestamp": 1591745820,
"price": 321.655,
"open": 321.68,
"high": 321.7,
"low": 321.61,
"close": 321.61,
"volume": 2608,
"vwap": 321.66511
}
type PriceTick struct {
Timestamp int64 `json:"timestamp"`
Price float64 `json:"price"`
Volume float64 `json:"volume"`
Open float64 `json:"open"`
High float64 `json:"high"`
Low float64 `json:"low"`
Close float64 `json:"close"`
VWAP float64 `json:"vwap"`
}
Ideally we want Timestamp
to be a time.Time
object rather than an int64
.
Go lets you do custom json decoding on an object by defining the UnmarshalJSON
interface method on a struct. But we don't want to do it for the entire PriceTick
object, just the timestamp
property.
We can define a custom Timestamp
struct to hold the time object, but then you have to dig into the object whenever you want to access the time.Time
object it's holding, which is not ideal if we want to replicate the native object structure we're reading from JSON.
This is where embedded structs shine.
We can modify the definition above to point to a struct that effectively wraps a time.Time
object, and only do custom json decoding on that object.
// Timestamp wraps a time object encoded as a int64 unix timestamp
// This can be used in structs and it automatically handles decoding
// int64 unix timestamps into a time.Time object.
type Timestamp struct {
time.Time
}
// PriceTick is a tick returned from the TimeAndSales endpoint
type PriceTick struct {
Timestamp Timestamp `json:"timestamp"`
Time string `json:"time"`
Price float64 `json:"price"`
Volume float64 `json:"volume"`
Open float64 `json:"open"`
High float64 `json:"high"`
Low float64 `json:"low"`
Close float64 `json:"close"`
VWAP float64 `json:"vwap"`
}
// UnmarshalJSON decodes an int64 timestamp into a time.Time object
func (p *Timestamp) UnmarshalJSON(bytes []byte) error {
// 1. Decode the bytes into an int64
var raw int64
err := json.Unmarshal(bytes, &raw)
if err != nil {
fmt.Printf("error decoding timestamp: %s\n", err)
return err
}
// 2 - Parse the unix timestamp
*&p.Time = time.Unix(raw, 0)
return nil
}
By embedding the time.Time
property in the Timestamp
struct without a property name, we can call any time.Time
methods directly on a Timestamp
object.
From the Go documentation on embedding:
When we embed a type, the methods of that type become methods of the outer type, but when they are invoked the receiver of the method is the inner type, not the outer one.