Skip to content

Instantly share code, notes, and snippets.

@jimmysawczuk
Last active November 4, 2017 03:45
Show Gist options
  • Save jimmysawczuk/70daaf968da4c29974fc1a6338d9c27c to your computer and use it in GitHub Desktop.
Save jimmysawczuk/70daaf968da4c29974fc1a6338d9c27c to your computer and use it in GitHub Desktop.
unflattens a JSON object and makes it whole again
package main
import (
"encoding/json"
"fmt"
"strconv"
"strings"
)
func main() {
// Here's our sample unflattened object...
expected := map[string]interface{}{}
json.Unmarshal([]byte(`{
"a": "foo",
"b": {
"baz": "buzz",
"foo": "bizz"
},
"c": [
"1",
{
"baz": "bar",
"foo": "baz"
},
"biz"
],
"d": [
[
"foo",
"bar"
],
[
"bizz",
"buzz",
"bang"
]
]
}`), &expected)
// ...and here's how it's represented as a flattened object. Note how nested objects have dots in the keys,
// and arrays (which are basically objects with anonymous keys) are indicated with numbers.
in := map[string]interface{}{}
json.Unmarshal([]byte(`{
"a": "foo",
"b.baz": "buzz",
"b.foo": "bizz",
"c.0": "1",
"c.1.baz": "bar",
"c.1.foo": "baz",
"c.2": "biz",
"d.0.0": "foo",
"d.0.1": "bar",
"d.1.0": "bizz",
"d.1.1": "buzz",
"d.1.2": "bang"
}`), &in)
// Unflatten it! Start with a prefix of "".
res := unflatten(in, "")
// Print the results.
fmt.Println(mustmarshal(in))
fmt.Println("----")
fmt.Println(mustmarshal(res))
fmt.Println("----")
fmt.Println(mustmarshal(expected))
fmt.Println("expected == unflattened:", mustmarshal(expected) == mustmarshal(res))
}
func unflatten(in map[string]interface{}, prefix string) map[string]interface{} {
// Our results are going in a map[string]interface{}, no matter what.
out := map[string]interface{}{}
// Iterate over the entire map.
for k, v := range in {
// If we have a prefix like "a." that means we're parsing the "a" key of the object; we don't care about any
// other keys, so we can skip them if they don't have this prefix. We'll get to them later.
if !strings.HasPrefix(k, prefix) {
continue
}
// Remove the prefix from what we're working with so we're only concerned with the end of our key, then
// split on ".".
ks := strings.Split(strings.Replace(k, prefix, "", 1), ".")
// If the split key is only one string long, we can set it in our output map and we're done with this key.
if len(ks) == 1 {
out[ks[0]] = v
continue
}
// Otherwise, we need to recurse deeper into that key. Append this key to the prefix, call unflatten
// recursively.
m := unflatten(in, prefix+ks[0]+".")
// Check to see if we have a key that looks like "c.0"; if we do, we're dealing with a slice.
_, err := strconv.ParseInt(ks[1], 10, 64)
if err != nil {
// It's an object, so we can just assign the results of the recursive call to the output map.
out[ks[0]] = m
} else {
// It's a slice, so we convert the map to a slice and assign it to the output map.
out[ks[0]] = convertToSlice(m)
}
}
// Return the output map!
return out
}
// mustmarshal is a helper function that ignores error handling for json.MarshalIndent and returns a string.
func mustmarshal(v interface{}) string {
by, _ := json.MarshalIndent(v, "", " ")
// by, _ := json.Marshal(v)
return string(by)
}
// convertToSlice iterates over a map and returns a slice with its values, preserving order based on the numerical
// string map keys.
func convertToSlice(in map[string]interface{}) []interface{} {
out := make([]interface{}, len(in))
for i, v := range in {
idx, _ := strconv.Atoi(i)
out[idx] = v
}
return out
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment