go – Adding an attribute to a nested map[string]interface{} in Golang-ThrowExceptions

Exception or error:

I am dealing with data that is of the type map[string]interface{}. it can have unlimited number of nested objects inside (map[string]interface{}) types.

EDIT: This data comes from mongodb. I can’t really apply golang’s struct here because the attributes vary from document to document. All I want to do is get the most deeply nested object, add a new attribute to it and make sure the entire data object is updated after.

data["person"] = map[string]interface{}{
    "peter": map[string]interface{}{
        "scores": map[string]interface{}{
            "calculus": 88,
            "algebra":  99,
            "golang":   89,
        },
    },
}

This data is coming from a remote API and I have no idea of the properties inside. All I want to add is add new attribute inside the last object (in this case “scores”), and lets say with this new attribute (“physics”) the data would look like this

data["person"] = map[string]interface{}{
    "peter": map[string]interface{}{
        "scores": map[string]interface{}{
            "calculus": 88,
            "algebra":  99,
            "golang":   89,
            "physics":  95,
        },
    },
}

I am not sure how I could get that attribute added to the very last object.

I did recursive type checking and was able to get each field and print its value. But because maps are not referential I cannot add a value to the original map when I reach the map with the values that are not complex types.

package main

import "fmt"

func main() {

    data := make(map[string]interface{})
    data["person"] = map[string]interface{}{
        "peter": map[string]interface{}{
            "scores": map[string]interface{}{
                "calculus": 88,
                "algebra":  99,
                "golang":   89,
            },
        },
    }

    parseMap(data)
}


func parseMap(aMap map[string]interface{}) interface{} {
    var retVal interface{}

    for _, val := range aMap {
        switch val.(type) {
        case map[string]interface{}:
            retVal = parseMap(val.(map[string]interface{}))
        //case []interface{}:
        //  retVal = parseArray(val.([]interface{}))
        default:
            //here i would have done aMap["physics"] = 95 if I could access the original map by reference, but that is not possible

            retVal = aMap

        }
    }

    return retVal
}
How to solve:

According the comments on the question, the goal is to set a value in the most deeply nested map.

Use the following function to find a map at the greatest nesting level. If there is more than one map at the greatest nesting level, this function returns an arbitrary one of those maps.

func findDeepest(m map[string]interface{}) (int, map[string]interface{}) {
    depth := 0
    candidate := m
    for _, v := range m {
        if v, ok := v.(map[string]interface{}); ok {
            d, c := findDeepest(v)
            if d+1 > depth {
                depth = d + 1
                candidate = c
            }
        }
    }
    return depth, candidate
}

Use it like this to set a value in the deeply nested map:

_, m := findDeepest(data)
m["physics"] = 95

Run it on the playground.

Answer´╝Ü

Try to avoid working with the raw map[string]interface{} type as much as you can. The Go encoding/json file can deal with string-keyed maps just fine, and hopefully the remote API has some sort of specification for what you’re dealing with. (You know that you’re expecting a person top-level key and scores in a specific point in the hierarchy, for example.)

I’m assuming the remote API is JSON-over-HTTP. You might model its structure as

type Input struct {
    Person map[string]Person `json:"person"`
}

type Person struct {
    Scores map[string]int `json:"scores"`
}

Once you’ve json.Unmarshal()ed data into this structure, you can directly set

data.Person["peter"].Scores["physics"] = 95

and then json.Marshal() the result again. https://play.golang.org/p/qoAVFodSvK2 has a complete example.

If you really wanted to directly manipulate the map[string]interface{} structure, I’d suggest splitting each “level” into a separate function call

func ParseTopLevel(data map[string]interface{}) {
    switch peter := data["peter"].(type) {
    case map[string]interface{}:
        ParsePeter(peter)
    }
}

map types are passed by reference, so when you get to the bottom of the stack you can directly set scores["physics"] = 95. (In your original code, I’d be surprised if you can’t directly set aMap["physics"] as you propose, though it’s rather imprecise on what gets set; compare https://play.golang.org/p/VuTjcjezwwU.)

Leave a Reply

Your email address will not be published. Required fields are marked *