Created
July 16, 2019 10:17
-
-
Save pulsejet/5197a2c67b9a91af7ce74a8923fd65ba to your computer and use it in GitHub Desktop.
Check if a JSON object is subset of another (useful for testing)
This file contains 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
// JSONDiff represents the whole diff | |
type JSONDiff struct { | |
Rows []JSONDiffRow | |
} | |
// JSONDiffRow represents a single non-existent subset item | |
type JSONDiffRow struct { | |
Key string | |
Expected interface{} | |
Got interface{} | |
} | |
// PrintJSONDiff prints JSONDiff to STDERR | |
func PrintJSONDiff(diff *JSONDiff) { | |
data := make([][]string, 0) | |
for _, r := range diff.Rows { | |
data = append(data, []string{r.Key, fmt.Sprintf("%s", r.Expected), fmt.Sprintf("%s", r.Got)}) | |
} | |
table := tablewriter.NewWriter(os.Stderr) | |
table.SetHeader([]string{"Key", "Expected", "Got"}) | |
for _, v := range data { | |
table.Append(v) | |
} | |
table.Render() | |
} | |
// JSONIsSubset checks if a is a subset json of b | |
func JSONIsSubset(a, b []byte) (bool, *JSONDiff, error) { | |
return jsonIsSubsetR(a, b, nil, nil) | |
} | |
func jsonIsSubsetR(a, b []byte, diff *JSONDiff, prefix interface{}) (bool, *JSONDiff, error) { | |
// Initialize | |
if diff == nil { | |
diff = &JSONDiff{} | |
} | |
if diff.Rows == nil { | |
diff.Rows = make([]JSONDiffRow, 0) | |
} | |
// Prefix for keeping around more info (path of the diffs) | |
sprefix := "" | |
if prefix != nil { | |
sprefix = prefix.(string) | |
} | |
// Unmarshal both interfaces. If something fails here, we have nothing to do | |
// jai: JSON A Interface | |
// jbi: JSON B Interface | |
var jai, jbi interface{} | |
if err := json.Unmarshal(a, &jai); err != nil { | |
return false, nil, err | |
} | |
if err := json.Unmarshal(b, &jbi); err != nil { | |
return false, nil, err | |
} | |
// Switch JSON (map) or array of JSON (array of interface) | |
// ja: JSON A (map or []interface) | |
// jb: JSON B (map or []interface) | |
switch ja := jai.(type) { | |
case map[string]interface{}: | |
// Cast B to same type as A | |
// TODO: Add a check to see if this fails | |
jb := jbi.(map[string]interface{}) | |
// Iterate all keys of ja and check if each is present | |
// and equal to the same key in jb | |
for k, vu := range ja { | |
switch vu.(type) { | |
// A primitive value such as string or number will be compared natively | |
default: | |
// Check if we have the key at all | |
if val, ok := jb[k]; ok { | |
// Check if the key matches if we have it | |
if vu != val { | |
diff.Rows = append(diff.Rows, JSONDiffRow{ | |
Key: fmt.Sprintf("%s/%s", sprefix, k), Expected: vu, Got: jb[k]}) | |
} | |
} else { | |
// We didn't find a key we wanted | |
diff.Rows = append(diff.Rows, JSONDiffRow{ | |
Key: fmt.Sprintf("%s/%s", sprefix, k), Expected: vu, Got: "NOT FOUND"}) | |
} | |
// Compare nested json by calling this function recursively | |
case map[string]interface{}, []interface{}: | |
sja, err := json.Marshal(vu) | |
if err != nil { | |
return false, nil, err | |
} | |
sjb, err := json.Marshal(jb[k]) | |
if err != nil { | |
return false, nil, err | |
} | |
_, _, err = jsonIsSubsetR(sja, sjb, diff, fmt.Sprintf("%s/%s", sprefix, k)) | |
if err != nil { | |
return false, nil, err | |
} | |
} | |
} | |
// Compare arrays | |
case []interface{}: | |
// Case jbi to an array as well | |
// TODO: Add a check to see if this fails | |
jb := jbi.([]interface{}) | |
// Check if length is equal first | |
if len(jb) != len(ja) { | |
// Length not equal so that is not good | |
diff.Rows = append(diff.Rows, JSONDiffRow{ | |
Key: fmt.Sprintf("%s", sprefix), Expected: fmt.Sprintf("LEN=%d", len(ja)), Got: fmt.Sprintf("LEN=%d", len(jb))}) | |
} else { | |
// Recurse for each object inside | |
for i, x := range ja { | |
sja, err := json.Marshal(x) | |
if err != nil { | |
return false, nil, err | |
} | |
sjb, err := json.Marshal(jb[i]) | |
if err != nil { | |
return false, nil, err | |
} | |
_, _, err = jsonIsSubsetR(sja, sjb, diff, fmt.Sprintf("%s[%d]", sprefix, i)) | |
if err != nil { | |
return false, nil, err | |
} | |
} | |
} | |
// Compare primitive types directly | |
default: | |
return jai == jbi, diff, nil | |
} | |
// No diff means all keys in A were found and equal in B | |
return diff == nil || len(diff.Rows) == 0, diff, nil | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I had a panic when comparing 2 jsons out of which one was indeed a subset of the other... I fixed it by doing a nil check after your "// TODO: Add a check to see if this fails" comments, e.g:
// TODO: Add a check to see if this fails
if jbi == nil {
return true, diff, nil
}
jb := jbi.([]interface{})