Question

I'm currently using the Jackson JSON library for Java to compare a few JSON objects.

The JSON objects contain the result of some mathematical computations. For instance:

{
  name:"result_set_1"
  result:[0.123151351353,1.0123151533,2.0123051353]
}

When comparing these with a reference implementation, I notice that some browsers produce slightly different results. This is fine, but I need to somehow make sure that I have a tolerance when comparing numbers.

JSONNode.equals() actually does a very nice deep-equals, but it compares Numbers in a way that forces them to be perfectly equal. I need to add a tolerance.

Is there a way to do a deep equals with a tolerance?

The only way right now I have found is to iterate over every single node, check if it is a number, and do a tolerance check instead of an equals. But this method is quite bulky because you have to check whether the node is an object, an array, a string... etc... and do specific things for each. I just want custom behavior for Numbers.

Is there a more elegant way of doing this? Any 3rd party libraries?

Était-ce utile?

La solution

Foreword: configure your ObjectMapper with DeserializationFeature.USE_BIGDECIMAL_FOR_FLOATS; by default Jackson will deserialize "non integer" JSON Numbers to doubles but if you do so you'll lose precision.

Here is an example adapted from one Equivalence<JsonNode> which I have written; you'd have to override the doNumEquivalent() and doNumHash() to fit your needs. It means that you use Guava but, really, you should.

Note that yes, there is a hash; you may or may not need it but it does not hurt to do so, especially since it means you'll be able to use a Set<Equivalence.Wrapper<JsonNode>> (you'd have to .add(myEquivalence.wrap(myNode)).

Note 2: NodeType is a specific class of mine; with "core Jackson" you want to use the .getNodeType() of JsonNode instead.

Usage: if (myEquivalence.equivalent(a, b)) // etc etc

public abstract class JsonNumEquivalence
    extends Equivalence<JsonNode>
{
    // Implement!
    protected abstract boolean doNumEquivalent(final JsonNode a, final JsonNode b);

    // Implement!
    protected abstract int doNumHash(final JsonNode t);

    @Override
    protected final boolean doEquivalent(final JsonNode a, final JsonNode b)
    {
        /*
         * If both are numbers, delegate to the helper method
         */
        if (a.isNumber() && b.isNumber())
            return doNumEquivalent(a, b);

        final NodeType typeA = NodeType.getNodeType(a);
        final NodeType typeB = NodeType.getNodeType(b);

        /*
         * If they are of different types, no dice
         */
        if (typeA != typeB)
            return false;

        /*
         * For all other primitive types than numbers, trust JsonNode
         */
        if (!a.isContainerNode())
            return a.equals(b);

        /*
         * OK, so they are containers (either both arrays or objects due to the
         * test on types above). They are obviously not equal if they do not
         * have the same number of elements/members.
         */
        if (a.size() != b.size())
            return false;

        /*
         * Delegate to the appropriate method according to their type.
         */
        return typeA == NodeType.ARRAY ? arrayEquals(a, b) : objectEquals(a, b);
    }

    @Override
    protected final int doHash(final JsonNode t)
    {
        /*
         * If this is a numeric node, delegate to the helper method
         */
        if (t.isNumber())
            return doNumHash(t);

        /*
         * If this is a primitive type (other than numbers, handled above),
         * delegate to JsonNode.
         */
        if (!t.isContainerNode())
            return t.hashCode();

        /*
         * The following hash calculations work, yes, but they are poor at best.
         * And probably slow, too.
         *
         * TODO: try and figure out those hash classes from Guava
         */
        int ret = 0;

        /*
         * If the container is empty, just return
         */
        if (t.size() == 0)
            return ret;

        /*
         * Array
         */
        if (t.isArray()) {
            for (final JsonNode element: t)
                ret = 31 * ret + doHash(element);
            return ret;
        }

        /*
         * Not an array? An object.
         */
        final Iterator<Map.Entry<String, JsonNode>> iterator = t.fields();

        Map.Entry<String, JsonNode> entry;

        while (iterator.hasNext()) {
            entry = iterator.next();
            ret = 31 * ret
                + (entry.getKey().hashCode() ^ doHash(entry.getValue()));
        }

        return ret;
    }

    private boolean arrayEquals(final JsonNode a, final JsonNode b)
    {
        /*
         * We are guaranteed here that arrays are the same size.
         */
        final int size = a.size();

        for (int i = 0; i < size; i++)
            if (!doEquivalent(a.get(i), b.get(i)))
                return false;

        return true;
    }

    private boolean objectEquals(final JsonNode a, final JsonNode b)
    {
        /*
         * Grab the key set from the first node
         */
        final Set<String> keys = Sets.newHashSet(a.fieldNames());

        /*
         * Grab the key set from the second node, and see if both sets are the
         * same. If not, objects are not equal, no need to check for children.
         */
        final Set<String> set = Sets.newHashSet(b.fieldNames());
        if (!set.equals(keys))
            return false;

        /*
         * Test each member individually.
         */
        for (final String key: keys)
            if (!doEquivalent(a.get(key), b.get(key)))
                return false;

        return true;
    }
}
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top