Skip to content

Instantly share code, notes, and snippets.

@alexkappa
Last active July 8, 2024 14:35
Show Gist options
  • Save alexkappa/4b541a712dc06c047a38b005178978b5 to your computer and use it in GitHub Desktop.
Save alexkappa/4b541a712dc06c047a38b005178978b5 to your computer and use it in GitHub Desktop.
type FleetVehicle interface {
MakeAndModel() string
}
type Vehicle struct {
Type string `json:"type"`
Make string `json:"make"`
Model string `json:"model"`
}
func (v Vehicle) MakeAndModel() string {
return fmt.Sprintf("%s %s", v.Make, v.Model)
}
type Car struct {
Vehicle
SeatingCapacity int `json:"seatingCapacity"`
TopSpeed float64 `json:"topSpeed"`
}
type Truck struct {
Vehicle
PayloadCapacity float64 `json:"payloadCapacity"`
}
type Bike struct {
Vehicle
TopSpeed float64 `json:"topSpeed"`
}
type Fleet struct {
Vehicles []FleetVehicle `json:"-"`
RawVehicles []json.RawMessage `json:"vehicles"`
}
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))
}
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 FleetVehicle
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
}
import (
"encoding/json"
"testing"
)
func TestJSONMarshal(t *testing.T) {
fleet := &Fleet{
Vehicles: []FleetVehicle{
&Car{Vehicle{"car", "Mercedes-Benz", "AMG C 63"}, 4, 250},
&Bike{Vehicle{"bike", "Triumph", "Bonneville T120"}, 193.12},
},
}
b, err := json.MarshalIndent(&fleet, "", " ")
if err != nil {
t.Fatal(err)
}
t.Logf("%s\n", b)
}
func TestJSONUnmarshal(t *testing.T) {
const body = `{
"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}
]
}`
var f *Fleet
err := json.Unmarshal([]byte(body), &f)
if err != nil {
t.Fatal(err)
}
for _, vehicle := range f.Vehicles {
if _, ok := vehicle.(FleetVehicle); !ok {
t.Fatal("expected type to implement FleetVehicle")
}
switch v := vehicle.(type) {
case *Car:
t.Logf("%s has a seating capacity of %d and a top speed of %.2f km/h\n",
v.MakeAndModel(),
v.SeatingCapacity,
v.TopSpeed)
case *Truck:
t.Logf("%s has a payload capacity of %.2f kg\n",
v.MakeAndModel(),
v.PayloadCapacity)
case *Bike:
t.Logf("%s has a top speed of %.2f km/h\n",
v.MakeAndModel(),
v.TopSpeed)
}
}
}
=== RUN TestJSONMarshal
--- PASS: TestJSONMarshal (0.00s)
json_test.go:22: {
"vehicles": [
{
"type": "car",
"make": "Mercedes-Benz",
"model": "AMG C 63",
"seatingCapacity": 4,
"topSpeed": 250
},
{
"type": "bike",
"make": "Triumph",
"model": "Bonneville T120",
"topSpeed": 193.12
}
]
}
=== RUN TestJSONUnmarshal
--- PASS: TestJSONUnmarshal (0.00s)
json_test.go:45: BMW M3 has a seating capacity of 4 and a top speed of 250.00 km/h
json_test.go:50: Volvo FH has a payload capacity of 40000.00 kg
json_test.go:54: Yamaha YZF-R1 has a top speed of 293.00 km/h
PASS
@inikolaev
Copy link

I'm new to Go, but is there any way to make this deserialisation logic reusable? E.g. could we add some custom tags to the vehicles to tell the name of discriminator column and subtype mapping?

Something like:

type Fleet struct {
    Vehicles []FleetVehicle `json:"vehicles" discriminator:"type,car=Car,truck=Truck,bike=Bike"`
}

And then have some magic which can look into these tags and instantiate?

@alexkappa
Copy link
Author

alexkappa commented Mar 14, 2022

Hi @inikolaev, as far as I'm aware there is no such functionality in encoding/json. My attempt was intended at replicating this functionality, but of course, it requires additional effort. One of the third-party JSON packages perhaps?

@jasonm-unity
Copy link

note that strictly speaking, RawVehicles []json.RawMessage `json:"vehicles"` does not need to be part of the Fleet type.
It can be local to the custom marshalling functions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment