Javier Carmona
Javier Carmona

Reputation: 547

Golang elegantly JSON decode different structures

I have different structures that share a field and I need to decode a JSON file into its corresponding structure in Go.

Example:

type Dog struct {
  AnimalType string //will always be "dog"
  BarkLoudnessLevel int
}

type Cat struct {
  AnimalType string //will always be "cat"
  SleepsAtNight bool
}

If I am receiving one of these structures as a JSON string, what would be the most elegant way of parsing it into its proper structure?

Upvotes: 5

Views: 5250

Answers (1)

photoionized
photoionized

Reputation: 5232

So, there are a couple ways of doing this, but the easiest is probably deserializing the payload twice and having conditional branches based off of the "AnimalType" attribute in your payload. Here's a simple example using an intermediate deserialization model:

package main

import (
  "fmt"
  "encoding/json"
)

type Dog struct {
  AnimalType string //will always be "dog"
  BarkLoudnessLevel int
}

type Cat struct {
  AnimalType string //will always be "cat"
  SleepsAtNight bool
}

var (
  payloadOne = `{"AnimalType":"dog","BarkLoudnessLevel":1}`
  payloadTwo = `{"AnimalType":"cat","SleepsAtNight":false}`
)

func main() {
  parseAnimal(payloadOne)
  parseAnimal(payloadTwo)
}

func parseAnimal(payload string) {
  animal := struct{
    AnimalType string
  }{} 
  if err := json.Unmarshal([]byte(payload), &animal); err != nil {
    panic(err)
  }
  switch animal.AnimalType {
  case "dog":
    dog := Dog{}
    if err := json.Unmarshal([]byte(payload), &dog); err != nil {
      panic(err)
    }
    fmt.Printf("Got a dog: %v\n", dog)
  case "cat":
    cat := Cat{}
    if err := json.Unmarshal([]byte(payload), &cat); err != nil {
      panic(err)
    }
    fmt.Printf("Got a cat: %v\n", cat)
  default:
    fmt.Println("Unknown animal")
  }
}

See it in action here.


IMO a better way of approaching this is moving the "metadata" for the payload into a parent structure, though this requires modifying the expected json payload. So, for example, if you were working with payloads that looked like:

{"AnimalType":"dog", "Animal":{"BarkLoudnessLevel": 1}}

Then you could use something like json.RawMessage to partially parse the structure and then conditionally parse the rest as needed (rather than parsing everything twice)--also results in a nicer separation of structure attributes. Here's an example of how you'd do that:

package main

import (
    "encoding/json"
    "fmt"
)

type Animal struct {
    AnimalType string
    Animal     json.RawMessage
}

type Dog struct {
    BarkLoudnessLevel int
}

type Cat struct {
    SleepsAtNight bool
}

var (
    payloadOne = `{"AnimalType":"dog", "Animal":{"BarkLoudnessLevel": 1}}`
    payloadTwo = `{"AnimalType":"cat", "Animal":{"SleepsAtNight": false}}`
)

func main() {
    parseAnimal(payloadOne)
    parseAnimal(payloadTwo)
}

func parseAnimal(payload string) {
    animal := &Animal{}
    if err := json.Unmarshal([]byte(payload), &animal); err != nil {
        panic(err)
    }
    switch animal.AnimalType {
    case "dog":
        dog := Dog{}
        if err := json.Unmarshal(animal.Animal, &dog); err != nil {
            panic(err)
        }
        fmt.Printf("Got a dog: %v\n", dog)
    case "cat":
        cat := Cat{}
        if err := json.Unmarshal(animal.Animal, &cat); err != nil {
            panic(err)
        }
        fmt.Printf("Got a cat: %v\n", cat)
    default:
        fmt.Println("Unknown animal")
    }
}

And in action here.

Upvotes: 12

Related Questions