Json.Encode.Value equality

I was surprised by the following test results. I had expected all five tests to fail. Instead, {a:1} == {a:1, b:2} is True, and Expect.equal {a:1, b:2} {a:1} passes; where {a:1} stands for a Json.Encode.Value created by Json.Encode.object. (Note that there only three test failures, instead of five.)

Is this expected? I don’t see anything in the docs for Json.Encode.Value or Expect.Equal that indicate this. The docs for (==) do list Json.Encode.Value as a “problematic type”, but the context suggests that this is because a Value might contain a function, not that (==) also returns false positives even when the properties have non-function types.

(This came up while writing tests for my Encoder, to verify that it didn’t create any unexpected object properties in a case where a programming error could lead it to do so.)

module JsonValueTests exposing (..)

import Expect exposing (Expectation)
import Json.Encode as Encode
import Test exposing (..)


suite : Test
suite =
    let
        obj1 =
            Encode.object [ ( "a", Encode.int 1 ) ]

        obj2 =
            Encode.object [ ( "a", Encode.int 1 ), ( "b", Encode.int 2 ) ]
    in
        describe "Json.Encode.Value"
            [ test "Expect.equal {a:1} {a:1, b:2}" <|
                \_ ->
                    Expect.equal obj1 obj2
            , test "Expect.equal {a:1, b:2} {a:1}" <|
                \_ ->
                    Expect.equal obj2 obj1
            , test "{a:1} == {a:1, b:2}" <|
                \_ ->
                    Expect.equal (obj1 == obj2) True
            , test "{a:1, b:2} == {a:1}" <|
                \_ ->
                    Expect.equal (obj2 == obj1) True
            , test "encode {a:1} == encode {a:1,b:2}" <|
                \_ ->
                    Encode.encode 0 obj1
                        |> Expect.equal (Encode.encode 0 obj2)
            ]
❯ elm-test tests/JsonValueTests.elm
Success! Compiled 0 modules.
Successfully generated /dev/null
Success! Compiled 1 module.
Successfully generated /Users/osteele/code/banyan/elm-stuff/generated-code/elm-community/elm-test/elmTestOutput.js

elm-test 0.18.12
----------------

Running 5 tests. To reproduce these results, run: elm-test --fuzz 100 --seed 2079329333 tests/JsonValueTests.elm

↓ JsonValueTests
↓ Json.Encode.Value
✗ Expect.equal {a:1} {a:1, b:2}

    { a = 1, b = 2 }
    ╷
    │ Expect.equal
    ╵
    { a = 1 }


↓ JsonValueTests
↓ Json.Encode.Value
✗ {a:1, b:2} == {a:1}

    True
    ╷
    │ Expect.equal
    ╵
    False


↓ JsonValueTests
↓ Json.Encode.Value
✗ encode {a:1} == encode {a:1,b:2}

    "{\"a\":1}"
    ╷
    │ Expect.equal
    ╵
    "{\"a\":1,\"b\":2}"



TEST RUN FAILED

Duration: 285 ms
Passed:   2
Failed:   3

There are more cases than just function values where Value is a problematic type for equality, and this happens to be one of them. There’s no way for the equality kernel function to distinguish between the JavaScript representations of the Elm types it has to work on and anything you can represent as a Value. Eventually it falls through to the comparison case for record types, where it tries to check every key in the lefthand value against the key in the righthand value. This is why it only fails in the cases where the record with two keys is the lefthand value. It tries to find b in the object that has no b. In the reversed situation, it stops checking after a because it’s making an assumption that it’s comparing records and the keys will always match.

2 Likes

I ended up using the following in my tests:

expectJsonEqual : Encode.Value -> Encode.Value -> Expectation
expectJsonEqual a =
    Expect.all
        [ Expect.equal a
        , flip Expect.equal a
        ]

When it reports an error, it’s not consistent about the order of the value and the expectation (it depends on which side has the extra key), but it’s good enough for my purposes for now.

This PR attempts to improve the documentation.