I'm trying to find a way to use one JSON string as a "template" of sorts to apply to another JSON string. For instance, if my template looks as follows:
{
"id": "1",
"options": {
"leatherseats": "1",
"sunroof": "1"
}
}
which I then apply to the following JSON string:
{
"id": "831",
"serial": "19226715",
"options": {
"leatherseats": "black",
"sunroof": "full",
"fluxcapacitor": "yes"
}
}
I'd like a resultant JSON string as follows:
{
"id": "831",
"options": {
"leatherseats": "black",
"sunroof": "full",
}
}
Unfortunately I can't rely on either the template nor the input to be of a fixed format so I can't marshall/unmarshall into defined interfaces.
I got as far as writing a recursive function that traverses the template to construct a slice of string with the name of each node that is to be included.
func traverseJSON(key string, value interface{}) []string {
var retval []string
unboxed, ok := value.(map[string]interface{})
if ok {
for newkey, newvalue := range unboxed {
retval = append(retval, recurse(fmt.Sprintf("%s.%s", key, newkey), newvalue)...)
}
} else {
retval = append(retval, fmt.Sprintf("%s", key))
}
return retval
}
I call this function as follows:
template := `my JSON template here`
var result map[string]interface{}
json.Unmarshal([]byte(template), &result)
var nodenames []string
nodenames = append(nodenames, traverseJSON("", result)...)
I was then going to write a second function that takes this slice of node names to construct a JSON string from the input JSON string but ran out of steam and started thinking that I might be on the wrong track anyway.
Any help on this would be appreciated.
Simply create a function which "clones" a map based on a template and a source map.
The solution would iterate over the entries of the template map, and for each (k, v)
pair generate an entry in the destination map as follows:
If v
is not a map, simply get the value for the k
key from the source map, and use this in the destination.
If v
is also a map, then call this "cloner" recursively with the new template map being v
and the new source being the value from the source for the k
key. The result of this recursive call will be the value for the k
key in the destination map.
This is how it could look like:
func procMap(tmpl, src map[string]interface{}) (dst map[string]interface{}) {
dst = map[string]interface{}{}
for k, v := range tmpl {
if innerMap, ok := v.(map[string]interface{}); ok {
dst[k] = procMap(innerMap, src[k].(map[string]interface{}))
} else {
dst[k] = src[k]
}
}
return dst
}
And that's all.
Testing it:
// tmpljson is the template JSON
var tmpl map[string]interface{}
if err := json.Unmarshal([]byte(tmpljson), &tmpl); err != nil {
panic(err)
}
// srcjson is the source JSON
var src map[string]interface{}
if err := json.Unmarshal([]byte(srcjson), &src); err != nil {
panic(err)
}
dst := procMap(tmpl, src)
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
if err := enc.Encode(dst); err != nil {
panic(err)
}
Output with your example JSONs (try it on the Go Playground):
{
"id": "831",
"options": {
"leatherseats": "black",
"sunroof": "full"
}
}
Notes:
The solution assumes the source map conforms to the template. That is, if the template contains a map for some key, the source map is also expected to contain a map for the same key. If this cannot be guaranteed, the procMap()
function should be extended with a check to avoid a runtime panic, like this:
for k, v := range tmpl {
if innerMap, ok := v.(map[string]interface{}); ok {
if src2, ok2 := src[k].(map[string]interface{}); ok2 {
dst[k] = procMap(innerMap, src2)
} else {
log.Printf("src is not conform to template at key %q", k)
}
} else {
dst[k] = src[k]
}
}
Also note that JSON arrays (slices) are not treated in any special way, meaning if the template contains a slice, the value from the source is used as-is, and no recursion happens if the slice contains maps. The solution can easily be extended to handle slices too, which is left as an exercise for the reader.