Last active
November 4, 2017 03:45
-
-
Save jimmysawczuk/70daaf968da4c29974fc1a6338d9c27c to your computer and use it in GitHub Desktop.
unflattens a JSON object and makes it whole again
This file contains hidden or 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" | |
"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