Devyzr
Devyzr

Reputation: 347

How to compare JSON with varying order?

I'm attempting to implement testing with golden files, however, the JSON my function generates varies in order but maintains the same values. I've implemented the comparison method used here:

How to compare two JSON requests?

But it's order dependent. And as stated here by brad:

JSON objects are unordered, just like Go maps. If you're depending on the order that a specific implementation serializes your JSON objects in, you have a bug.

I've written some sample code that simulated my predicament:

package main

import (
    "bufio"
    "encoding/json"
    "fmt"
    "io/ioutil"
    "math/rand"
    "os"
    "reflect"
    "time"
)

type example struct {
    Name     string
    Earnings float64
}

func main() {
    slice := GetSlice()
    gfile, err := ioutil.ReadFile("testdata/example.golden")
    if err != nil {
        fmt.Println(err)
        fmt.Println("Failed reading golden file")
    }

    testJSON, err := json.Marshal(slice)
    if err != nil {
        fmt.Println(err)
        fmt.Println("Error marshalling slice")
    }

    equal, err := JSONBytesEqual(gfile, testJSON)
    if err != nil {
        fmt.Println(err)
        fmt.Println("Error comparing JSON")
    }

    if !equal {
        fmt.Println("Restults don't match JSON")
    } else {
        fmt.Println("Success!")
    }
}

func GetSlice() []example {
    t := []example{
        example{"Penny", 50.0},
        example{"Sheldon", 70.0},
        example{"Raj", 20.0},
        example{"Bernadette", 200.0},
        example{"Amy", 250.0},
        example{"Howard", 1.0}}
    rand.Seed(time.Now().UnixNano())
    rand.Shuffle(len(t), func(i, j int) { t[i], t[j] = t[j], t[i] })
    return t
}

func JSONBytesEqual(a, b []byte) (bool, error) {
    var j, j2 interface{}
    if err := json.Unmarshal(a, &j); err != nil {
        return false, err
    }
    if err := json.Unmarshal(b, &j2); err != nil {
        return false, err
    }
    return reflect.DeepEqual(j2, j), nil
}

func WriteTestSliceToFile(arr []example, filename string) {
    file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)

    if err != nil {
        fmt.Println("failed creating file: %s", err)
    }

    datawriter := bufio.NewWriter(file)
    marshalledStruct, err := json.Marshal(arr)
    if err != nil {
        fmt.Println("Error marshalling json")
        fmt.Println(err)
    }
    _, err = datawriter.Write(marshalledStruct)
    if err != nil {
        fmt.Println("Error writing to file")
        fmt.Println(err)
    }

    datawriter.Flush()
    file.Close()
}

Upvotes: 2

Views: 9528

Answers (2)

localhorst
localhorst

Reputation: 453

For unit testing, you could use assert.JSONEq from Testify. If you need to do it programatically, you could follow the code of the JSONEq function.

https://github.com/stretchr/testify/blob/master/assert/assertions.go#L1708

Upvotes: 4

user12258482
user12258482

Reputation:

JSON arrays are ordered. The json.Marshal function preserves order when encoding a slice to a JSON array.

JSON objects are not ordered. The json.Marshal function writes object members in sorted key order as described in the documentation.

The bradfitz comment JSON object ordering is not relevant to this question:

  • The application in the question is working with a JSON array, not a JSON object.
  • The package was updated to write object fields in sorted key order a couple of years after Brad's comment.

To compare slices while ignoring order, sort the two slices before comparing. This can be done before encoding to JSON or after decoding from JSON.

sort.Slice(slice, func(i, j int) bool {
     if slice[i].Name != slice[j].Name {
        return slice[i].Name < slice[j].Name
     }
     return slice[i].Earnings < slice[j].Earnings
})

Upvotes: 6

Related Questions