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!