Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Intersection of JSON strings

Tags:

json

string

go

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.

like image 377
Dewald Swanepoel Avatar asked Oct 07 '18 16:10

Dewald Swanepoel


1 Answers

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.

like image 85
icza Avatar answered Nov 01 '22 19:11

icza