Maintainable way to Unmarshal/Marshal Slice of Dynamic Types

I’m wondering what might be the most reasonable way to prevent writing a bunch of code every time I have to handle JSON like the following example. This is a potential response from a REST API I have no control over. It contains a list of pets where each could be one of an enum of 1+ types.

{
    "pets": [
        {
            "type": "Cat",
            "name": "Mittens",
            "is_angry": true
        },
        {
            "type": "Dog",
            "name": "Spot",
            "has_ball": false
        }
    ]
}

I have managed to write this to handle it. But it feels kind of hacky, especially if this is a pattern that could appear many times when interacting with the REST API.

package main

import (
	"encoding/json"
	"fmt"
)

type Pet struct {
	Type string `json:"type"`
	Name string `json:"name"`
}

type DynamicPet interface {
	isPet()
}

func (p Pet) isPet() {}

type Cat struct {
	Pet
	IsAngry bool `json:"is_angry"`
}

type Dog struct {
	Pet
	HasBall bool `json:"has_ball"`
}

var petTypeMap = map[string]func() DynamicPet{
	"Cat": func() DynamicPet { return &Cat{} },
	"Dog": func() DynamicPet { return &Dog{} },
}

const (
	CatType    = "Cat"
	DogType    = "Dog"
	PingusType = "Pingus"
)

type DynamicPetWrapper struct {
	Pet DynamicPet `json:"-"`
}

func (p *DynamicPetWrapper) UnmarshalJSON(data []byte) error {
	var typeData struct {
		Type string `json:"type"`
	}
	if err := json.Unmarshal(data, &typeData); err != nil {
		return err
	}

	petType, ok := petTypeMap[typeData.Type]
	if !ok {
		return fmt.Errorf("unknown pet type: %s", typeData.Type)
	}
	p.Pet = petType()

	if err := json.Unmarshal(data, p.Pet); err != nil {
		return err
	}

	return nil
}

func (p DynamicPetWrapper) MarshalJSON() ([]byte, error) {
	return json.Marshal(p.Pet)
}

type PetList struct {
	Pets []DynamicPetWrapper `json:"pets"`
}

func main() {
	// json example PetList
	jsonData := []byte(`
	{
		"pets": [
			{
				"type": "Cat",
				"name": "Mittens",
				"is_angry": true
			},
			{
				"type": "Dog",
				"name": "Spot",
				"has_ball": false
			}
		]
	}
	`)

	// deserialize json into PetList dynamically unmarshalling into the correct types
	var petList PetList
	if err := json.Unmarshal(jsonData, &petList); err != nil {
		fmt.Println("Error:", err)
		return
	}

	// iterate over the list of pets and do logic based on pet type
	for _, petWrapper := range petList.Pets {
		switch pet := petWrapper.Pet.(type) {
		case *Cat:
			fmt.Printf("Cat: is_angry %v\n", pet.IsAngry)
		case *Dog:
			fmt.Printf("Dog: has_ball %v\n", pet.HasBall)
		}
	}

	// serialize back to JSON to make sure it worked both ways
	jsonData2, err := json.Marshal(petList)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	fmt.Println(string(jsonData2))
}

One thing that keeps me up at night about this is that in Rust, I don’t need a wrapper type, or to write special marshaling logic, it just works, like magic, with half the code. And the compiler will yell at me if I haven’t handled any new type I might implement, thanks to how match arms work. I can expect to easily use the same types, without extra layering or complexity, everywhere in my code.

use serde::{Deserialize, Serialize};
use serde_json;

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
enum Pet {
    Cat(Cat),
    Dog(Dog),
}

#[derive(Serialize, Deserialize, Debug, Clone)]
struct Cat {
    name: String,
    is_angry: bool,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
struct Dog {
    name: String,
    has_ball: bool,
}

#[derive(Serialize, Deserialize, Debug)]
struct PetList {
    pets: Vec<Pet>,
}

fn main() {
    // json example PetList
    let json = r#"
        {
            "pets": [
                {
                    "type": "Cat",
                    "name": "Mittens",
                    "is_angry": true
                },
                {
                    "type": "Dog",
                    "name": "Spot",
                    "has_ball": false
                }
            ]
        }
    "#;

    // deserialize json into PetList dynamically unmarshalling into the correct types
    let pet_list: PetList = serde_json::from_str(json).unwrap();

    // can easily iterate over the list of pets and do logic based on pet type
    for pet in pet_list.pets.iter() {
        match pet {
            Pet::Cat(cat) => println!("Cat: is_angry {}", cat.is_angry),
            Pet::Dog(dog) => println!("Dog: has_ball {}", dog.has_ball),
        }
    }

    // serialize back to JSON to make sure it worked both ways
    let json = serde_json::to_string(&pet_list).unwrap();
    println!("{}", json);

}

I just feel like my code base will spiral out of control every time I have to interact with fancy JSON in Go. Thanks for any conversation on this topic!

I know it’s not ideally type-safe, but if the types of pets is unbounded and since we don’t know what your code needs to do with a pet, I’d be inclined to model a pet as type Pet map[string]any and add methods for Pet for Name and Type, and another for the boolean property if it is expected that all pets would have a custom boolean property as the dog and cat do.

This is how I would do it, it could be simpler without the base struct but…

package main

import (
	"encoding/json"
	"fmt"
)

type Pet interface {
	GetType() string
	GetName() string
	IsAngry() bool
	HasBall() bool
}
type PetBase struct {
	Name  string `json:"name"`
	Ptype string `json:"type"`
	Angry *bool  `json:"is_angry,omitempty"`
	Ball  *bool  `json:"has_ball,omitempty"`
}

func (p *PetBase) GetName() string {
	return p.Name
}
func (p *PetBase) GetType() string {
	return p.Ptype
}
func (p *PetBase) IsAngry() bool {
	if p.Angry != nil {
		return *p.Angry
	}
	return false
}
func (p *PetBase) HasBall() bool {
	if p.Ball != nil {
		return *p.Ball
	}
	return false
}

type Cat struct {
	PetBase
}
type Dog struct {
	PetBase
}

type PetList struct {
	Pets []PetBase `json:"pets"`
}

func main() {
	// json example PetList
	jsonData := []byte(`
	{
		"pets": [
			{
				"type": "Cat",
				"name": "Mittens",
				"is_angry": true
			},
			{
				"type": "Dog",
				"name": "Spot",
				"has_ball": false
			}
		]
	}
	`)

	// deserialize json into PetList dynamically unmarshalling into the correct types
	var data PetList
	if err := json.Unmarshal(jsonData, &data); err != nil {
		fmt.Println("Unmarshall Error:", err)
		return
	}
	fmt.Printf("PetList: %v\n", data)
	// iterate over the list of pets and do logic based on pet type
	for _, petb := range data.Pets {
		var mypet Pet
		switch petb.GetType() {
		case "Cat":
			mypet = &Cat{PetBase: petb}
		case "Dog":
			mypet = &Dog{PetBase: petb}
		}
		fmt.Printf("%s: is_angry %v\n", mypet.GetType(), mypet.IsAngry())
		fmt.Printf("%s: has_ball %v\n", mypet.GetType(), mypet.HasBall())
	}

	// serialize back to JSON to make sure it worked both ways
	jsonData2, err := json.Marshal(data)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	fmt.Println(string(jsonData2))
}