JSON polymorphism in Go

Serializing and deserializing polymorphic JSON into Go structs

Alex Kalyvitis
4 min readMar 15, 2020

Introduction

It is quite common in object-oriented languages to rely on polymorphism to restrict the range of types that can be used in a particular case. This can be achieved either by using an interface or by subtyping.

In Go we can use an interface to achieve polymorphism, but subtyping however is not possible as there is no traditional notion of inheritance (e.g. via subclassing). We can embed a struct within another, but that doesn’t achieve the same effect as subtyping.

Example

Let’s take an example that is commonly used to describe this problem. A fleet of vehicles which can be either cars, trucks, bikes or any special case of vehicle we can imagine.

{
"vehicles": [
{
"type": "car",
"make": "BMW",
"model": "M3",
"seatingCapacity": 4,
"topSpeed": 250
},
{
"type": "truck",
"make": "Volvo",
"model": "FH",
"payloadCapacity": 40000
},
{
"type": "bike",
"make": "Yamaha",
"model": "YZF-R1",
"topSpeed": 293
}
]
}

Types

Let’s attempt to define types that can serialize and deserialize this payload.

type Vehicle struct {
Type string `json:"type"`
Make string `json:"make"`
Model string `json:"model"`
}
type Car struct {
SeatingCapacity int `json:"seatingCapacity"`
TopSpeed float64 `json:"topSpeed"`
Vehicle
}
type Truck struct {
PayloadCapacity float64 `json:"payloadCapacity"`
Vehicle
}
type Bike struct {
TopSpeed float64 `json:"topSpeed"`
Vehicle
}
type Fleet struct {
Vehicles []interface{} `json:"vehicles"`
}

While Go will happily serialize our example payload using these data structures, there are some serious limitations.

Most importantly, when deserializing Go won’t be able to figure out when to deserialize into a Car, Truck or Bike. It will instead populate a map[string]interface{} which is not really what we want.

Also important is the fact that the vehicles are not limited much by the empty interface. In fact any type implements the empty interface.

To address the first issue, we’ll need to implement our own MarshalJSON and UnmarshalJSON methods for the Fleet type. We’ll also need a json.RawMessage field to represent our vehicles in their intermediate state.

type Fleet struct {
Vehicles []interface{} `json:"-"`
RawVehicles []json.RawMessage `json:"vehicles"`
}

Notice how the JSON tags now instruct Go’s json package to ignore the Vehicles field when serializing/deserializing but to use RawVehicles instead.

Methods

func (f *Fleet) UnmarshalJSON(b []byte) error {    type fleet Fleet    err := json.Unmarshal(b, (*fleet)(f))
if err != nil {
return err
}

for _, raw := range f.RawVehicles {
var v Vehicle
err = json.Unmarshal(raw, &v)
if err != nil {
return err
}
var i interface{}
switch v.Type {
case "car":
i = &Car{}
case "truck":
i = &Truck{}
case "bike":
i = &Bike{}
default:
return errors.New("unknown vehicle type")
}
err = json.Unmarshal(raw, i)
if err != nil {
return err
}
f.Vehicles = append(f.Vehicles, i)
}
return nil
}

What we’ve done here, is alias the Fleet type to fleet internally, so that json.Unmarshal doesn’t get trapped in an infinite loop. Once the unmarshaling takes place, f is populated with the contents of the payload. But until this point Vehicles is an empty slice, and RawVehicles is populated with all vehicles in the payload, but as raw JSON.

At this stage we will examine each item in RawVehicles by unmarshaling it into an intermediate representation which can help us understand what type of vehicle we need to unmarshal into. We could use a map[string]interface{} but since we already abstract some common properties into the Vehicle struct, we’ll just go ahead and use that.

Now that the type is known to us, we then use a switch statement to unmarshal the vehicle into its correct struct — one of Car, Truck or Bike. Then it’s only a matter of adding each car, truck or bike to the Vehicles slice.

Since we introduced the RawVehicles field and instructed the json package to ignore Vehicles we’ll need to implement the MarshalJSON method to ensure the correct serialization takes place. To do so, we will serialize each item in Vehicles and copy it to RawVehicles. Then let json.Marshal do what it usually does.

func (f *Fleet) MarshalJSON() ([]byte, error) {

type fleet Fleet
if f.Vehicles != nil {
for _, v := range f.Vehicles {
b, err := json.Marshal(v)
if err != nil {
return nil, err
}
f.RawVehicles = append(f.RawVehicles, b)
}
}
return json.Marshal((*fleet)(f))
}

With all structs and their methods in place, we can now deserialize into Go structs from this polymorphic JSON payload.

var f *Fleet
err := json.Unmarshal([]byte(body), &f)
if err != nil {
t.Fatal(err)
}
for _, vehicle := range f.Vehicles { switch v := vehicle.(type) {
case *Car:
fmt.Printf("%s %s has a seating capacity of %d and a top speed of %.2f km/h\n",
v.Make,
v.Model,
v.SeatingCapacity,
v.TopSpeed)
case *Truck:
fmt.Printf("%s %s has a payload capacity of %.2f kg\n",
v.Make,
v.Model,
v.PayloadCapacity)
case *Bike:
fmt.Printf("%s %s has a top speed of %.2f km/h\n",
v.Make,
v.Model,
v.TopSpeed)
}
}

Now that we addressed the correct serialization and deserialization, we can improve upon the example by replacing the empty interface with something that actually limits the types we can use in the Vehicles slice. In principle, we’ll only be replacing it with a more narrow interface, and make sure our vehicles implement that interface.

type FleetVehicle interface {
MakeAndModel() string
}

This interface is now much narrower. Only types that implement MakeAndModel() function are legal implementations of FleetVehicle. Let’s replace all instances of the empty interface and replace them with FleetVehicle.

type Fleet struct {
Vehicles []FleetVehicle `json:"-"`
RawVehicles []json.RawMessage `json:"vehicles"`
}

Conclusion

Using Go’s interfaces and a little creative use of the MarshalJSON and UnmarshalJSON methods, we were able to serialize and deserialize polymorphic JSON data structures into Go structs.

By replacing the empty interface with a smaller, more narrow interface, we allow ourselves to manipulate polymorphic objects without necessarily knowing their concrete implementation.

See the complete example here.

--

--