The Problem
Given a list of json objects of different types (lets say People and Places). You want to Unmarshal them into two lists. A list of all the people and a list of all the places.A bit more definition
Let's use this json string{ "things": [ { "name": "Alice", "age": 37 }, { "city": "Ipoh", "country": "Malaysia" }, { "name": "Bob", "age": 36 }, { "city": "Northampton", "country": "England" } ] }To help us write some code, let's give ourselves a function, which should be self explanatory:
func solution(jsonString []byte) ([]Person []Place) {}And some structures
type Person struct { Name string Age int } type Place struct { City string Country string }I've got two solutions to this problem. I would love to know of better ways.
SolutionA: map and type assert
If we tell json to unmarshal into a map we can get it to deal with the parts we know about, and the rest of it will go into an interface{}. As we loop over the json structures we use what we do know about the structures to pass the interface{} to some helper functions what will create one of our structs and add it to our list. Because the map we take in is a map[string]interface{} we will need to type assert our valuesfunc solutionA(jsonStr []byte) ([]Person, []Place) { persons := []Person{} places := []Place{} var data map[string][]map[string]interface{} err := json.Unmarshal(jsonStr, &data) if err != nil { fmt.Println(err) return persons, places } for i := range data["things"] { item := data["things"][i] if item["name"] != nil { persons = addPerson(persons, item) } else { places = addPlace(places, item) } } return persons, places } func addPerson(persons []Person, item map[string]interface{}) []Person { name, _ := item["name"].(string) age, _ := item["age"].(int) person := Person{name, age} persons = append(persons, person) return persons } func addPlace(places []Place, item map[string]interface{}) []Place { city, _ := item["city"].(string) country, _ := item["city"].(string) place := Place{city, country} places = append(places, place) return places }
SolutionB: Mixed Type struct
This solution involves creating an interim struct which can be used to represent either a person or a placetype Mixed struct { Name string `json:"name"` Age int `json:"age"` City string `json:"city"` Country string `json:"country"` }With this struct we can then unmarshal our json string into a list of these mixed types. As we loop over our Mixed structs we just need to examine each one to work out which type it represents, and then build the right struct from it
func solutionB(jsonStr []byte) ([]Person, []Place) { persons := []Person{} places := []Place{} var data map[string][]Mixed err := json.Unmarshal(jsonStr, &data) if err != nil { fmt.Println(err) return persons, places } for i := range data["things"] { item := data["things"][i] if item.Name != "" { persons = append(persons, Person{item.Name, item.Age}) } else { places = append(places, Place{item.City, item.Country}) } } return persons, places }These are just two ways I've used to solve these problems, I'd love to know how others have done it.
SolutionC: json.RawMessage (Updated 18Jan13)
Thanks to Jordan's comment and zemo on reddit there is another solution. Using the json.RawMessage structure in the json package we can delay unmarshalling the json structures in the list. We can then go through our list and unmarshal each of them into the correct typefunc solutionC(jsonStr []byte) ([]Person, []Place) { people := []Person{} places := []Place{} var data map[string][]json.RawMessage err := json.Unmarshal(jsonStr, &data) if err != nil { fmt.Println(err) return people, places } for _, thing := range data["things"] { people = addPersonC(thing, people) places = addPlaceC(thing, places) } return people, places } func addPersonC(thing json.RawMessage, people []Person) []Person { person := Person{} if err := json.Unmarshal(thing, &person); err != nil { fmt.Println(err) } else { if person != *new(Person) { people = append(people, person) } } return people } func addPlaceC(thing json.RawMessage, places []Place) []Place { place := Place{} if err := json.Unmarshal(thing, &place); err != nil { fmt.Println(err) } else { if place != *new(Place) { places = append(places, place) } } return places }Here's the full gist:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"encoding/json" | |
"fmt" | |
) | |
var jsonStr = []byte(` | |
{ | |
"things": [ | |
{ | |
"name": "Alice", | |
"age": 37 | |
}, | |
{ | |
"city": "Ipoh", | |
"country": "Malaysia" | |
}, | |
{ | |
"name": "Bob", | |
"age": 36 | |
}, | |
{ | |
"city": "Northampton", | |
"country": "England" | |
} | |
] | |
}`) | |
func main() { | |
personsA, placesA := solutionA(jsonStr) | |
fmt.Printf("%d %d\n", len(personsA), len(placesA)) | |
personsB, placesB := solutionB(jsonStr) | |
fmt.Printf("%d %d\n", len(personsB), len(placesB)) | |
personsC, placesC := solutionC(jsonStr) | |
fmt.Printf("%d %d\n", len(personsC), len(placesC)) | |
} | |
//Common to both solutions | |
type Person struct { | |
Name string | |
Age int | |
} | |
type Place struct { | |
City string | |
Country string | |
} | |
//Solution A | |
//Unmarshal into a map | |
//Type assert when we need it | |
func solutionA(jsonStr []byte) ([]Person, []Place) { | |
persons := []Person{} | |
places := []Place{} | |
var data map[string][]map[string]interface{} | |
err := json.Unmarshal(jsonStr, &data) | |
if err != nil { | |
fmt.Println(err) | |
return persons, places | |
} | |
for i := range data["things"] { | |
item := data["things"][i] | |
if item["name"] != nil { | |
persons = addPerson(persons, item) | |
} else { | |
places = addPlace(places, item) | |
} | |
} | |
return persons, places | |
} | |
func addPerson(persons []Person, item map[string]interface{}) []Person { | |
name, _ := item["name"].(string) | |
age, _ := item["age"].(int) | |
person := Person{name, age} | |
persons = append(persons, person) | |
return persons | |
} | |
func addPlace(places []Place, item map[string]interface{}) []Place { | |
city, _ := item["city"].(string) | |
country, _ := item["city"].(string) | |
place := Place{city, country} | |
places = append(places, place) | |
return places | |
} | |
//SolutionB | |
type Mixed struct { | |
Name string `json:"name"` | |
Age int `json:"age"` | |
City string `json:"city"` | |
Country string `json:"country"` | |
} | |
func solutionB(jsonStr []byte) ([]Person, []Place) { | |
persons := []Person{} | |
places := []Place{} | |
var data map[string][]Mixed | |
err := json.Unmarshal(jsonStr, &data) | |
if err != nil { | |
fmt.Println(err) | |
return persons, places | |
} | |
for i := range data["things"] { | |
item := data["things"][i] | |
if item.Name != "" { | |
persons = append(persons, Person{item.Name, item.Age}) | |
} else { | |
places = append(places, Place{item.City, item.Country}) | |
} | |
} | |
return persons, places | |
} | |
//SolutionC | |
func solutionC(jsonStr []byte) ([]Person, []Place) { | |
people := []Person{} | |
places := []Place{} | |
var data map[string][]json.RawMessage | |
err := json.Unmarshal(jsonStr, &data) | |
if err != nil { | |
fmt.Println(err) | |
return people, places | |
} | |
for _, thing := range data["things"] { | |
people = addPersonC(thing, people) | |
places = addPlaceC(thing, places) | |
} | |
return people, places | |
} | |
func addPersonC(thing json.RawMessage, people []Person) []Person { | |
person := Person{} | |
if err := json.Unmarshal(thing, &person); err != nil { | |
fmt.Println(err) | |
} else { | |
if person != *new(Person) { | |
people = append(people, person) | |
} | |
} | |
return people | |
} | |
func addPlaceC(thing json.RawMessage, places []Place) []Place { | |
place := Place{} | |
if err := json.Unmarshal(thing, &place); err != nil { | |
fmt.Println(err) | |
} else { | |
if place != *new(Place) { | |
places = append(places, place) | |
} | |
} | |
return places | |
} |