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.
Show Comments