在Go中使用未知键值从JSON创建字符串映射

I try to create a map of strings from a JSON with an undefined number of unknow key-values.

Here is my example JSON file:

{
         "localhost":
        {
                "tag": "dev_latest",
                "vhost": "localhost.com"
        },
        "development":
        {
                "tag": "dev_latest",
                "vhost": "dev.com"
        }
}

I want to create a map[string]string with value like this:

config := map[string]string{
    "localhost-tag":      "dev_latest",
    "localhost-vhost": "localhost.com,
    "development-tag":   "dev_latest,
    ...
}

To parse a JSON with "github.com/jmoiron/jsonq" with known values, is quite easy, but in this case, localhost can be anything and tag can be any other thing.

My entry point in my Go code is like this:

func ParseJson(){
    configPath := GetConfigPath()
    b, err := ioutil.ReadFile(configPath) 

     //  Here, I need to create my map of strings..

    return configKeyStr

}

Any help will be really appreciate.

Thanks!

Easy to do. Simply convert.

package main

import (
    "encoding/json"
    "fmt"
    "log"
)

const s = `
{
         "localhost":
        {
                "tag": "dev_latest",
                "vhost": "localhost.com"
        },
        "development":
        {
                "tag": "dev_latest",
                "vhost": "dev.com"
        }
}
`

func main() {
    var m map[string]interface{}
    err := json.Unmarshal([]byte(s), &m)
    if err != nil {
        log.Fatal(err)
    }
    mm := make(map[string]string)
    for k, v := range m {
        mm[k] = fmt.Sprint(v)
    }
    fmt.Println(mm)
}

UPDATE

Wrote flatten (maybe works as charm)

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "reflect"
)

const s = `
{
         "localhost":
        {
                "tag": "dev_latest",
                "vhost": "localhost.com"
        },
        "development":
        {
                "tag": "dev_latest",
                "vhost": "dev.com"
        }
}
`

func flatten(m map[string]interface{}) map[string]string {
    mm := make(map[string]string)
    for k, v := range m {
        switch reflect.TypeOf(v).Kind() {
        case reflect.Map:
            mv := flatten(v.(map[string]interface{}))
            for kk, vv := range mv {
                mm[k+"-"+kk] = vv
            }
        case reflect.Array, reflect.Slice:
            for kk, vv := range m {
                if reflect.TypeOf(vv).Kind() == reflect.Map {
                    mv := flatten(vv.(map[string]interface{}))
                    for kkk, vvv := range mv {
                        mm[k+"-"+kkk] = vvv
                    }
                } else {
                    mm[k+"-"+kk] = fmt.Sprint(vv)
                }
            }
        default:
            mm[k] = fmt.Sprint(v)
        }
    }
    return mm
}

func main() {
    var m map[string]interface{}
    err := json.Unmarshal([]byte(s), &m)
    if err != nil {
        log.Fatal(err)
    }
    b, _ := json.MarshalIndent(flatten(m), "", "  ")
    println(string(b))
}

You can't have this automatically, but you can range over the "internal" maps, and combine the outer keys with the inner keys using simple string concatenation (+ operator). Also it's recommended to unmarshal directly into a value of map[string]map[string]string so you don't need to use type assertions. Also no need to use any external libraries for this, the standard encoding/json package is perfectly enough for this.

Example:

var mm map[string]map[string]string
if err := json.Unmarshal([]byte(src), &mm); err != nil {
    panic(err)
}
config := map[string]string{}
for mk, m := range mm {
    for k, v := range m {
        config[mk+"-"+k] = v
    }
}
fmt.Println(config)

Output is as expected (try it on the Go Playground):

map[localhost-tag:dev_latest localhost-vhost:localhost.com
    development-tag:dev_latest development-vhost:dev.com]

Since in the question you mentioned undefined number of unknown key-values, you may need to deal with JSON document with unknown number of nesting level and having a value other than string. In this case, you need to Unmarshal json to map[string]interface{}, then use recursion to make flat map. Once the json document unmrashaled to map[string]interface{}, use the following function:

func flatMap(src map[string]interface{}, baseKey, sep string, dest map[string]string) {
    for key, val := range src {
        if len(baseKey) != 0 {
            key = baseKey + sep + key
        }
        switch val := val.(type) {
        case map[string]interface{}:
            flatMap(val, key, sep, dest)
        case string:
            dest[key] = val
        case fmt.Stringer:
            dest[key] = val.String()
        default:
            //TODO: You may need to handle ARRAY/SLICE

            //simply convert to string using `Sprintf`
            //NOTE: modify as needed.
            dest[key] = fmt.Sprintf("%v", val)
        }
    }
}

The working solution adapted from mattn answer at https://play.golang.org/p/9SQsbAUFdY

As pointed by mattn, you may have problem when you want to writeback the configuration value. In that case, use the existing library/framework.