Skip to content

SimpleJSON.JSONXXX breaks Dictionary key semantics (Equals not reflexive) + mutable hash codes in value nodes (JSONBool/JSONString/JSONNumber) #118

Description

@udlose

Describe the bug
I noticed that using SimpleJSON.JSONBool as a dictionary key: after adding an instance as a key, ContainsKey(sameInstance) returns false even before any mutation. This appears to be because JSONBool.Equals(object) returns true only when compared to a boxed bool, and returns false when compared to a JSONBool (including itself).

Several Issues:

  1. JSONBool.Equals is not reflexive (x.Equals(x) can be false), which breaks core .NET equality and collection semantics immediately.
  2. JSONBool/JSONString/JSONNumber have hash codes derived from mutable state, so they’re unsafe in hash-based scenarios after mutation (including implicit hashing in LINQ
  3. maybe-by-design???: cross-type equality (JSONNumber vs numeric primitives, JSONString vs string) is inherently non-symmetric, so it should either be avoided, carefully documented, or implemented via explicit comparisons/conversions rather than Equals

Mutable hash (JSONBool/JSONString/JSONNumber)
This can break any hash-based collection or algorithm if the node is mutated after being used as a key/element, for example:

  • HashSet<JSONBool> membership can "lose" an item after mutation for the same reason a dictionary key can
  • LINQ methods that build internal hash sets/lookups (e.g., Distinct, Except, Intersect, GroupBy, ToLookup) can behave inconsistently if elements/keys mutate while participating

To Reproduce
small repro:

using System;
using System.Collections.Generic;
using SimpleJSON;

public static class Program
{
    public static void Main()
    {
        const Int32 val = 1;
        SimpleJSON.JSONBool jsonBool = new SimpleJSON.JSONBool(true);
        Dictionary<SimpleJSON.JSONBool, Int32> dict = new Dictionary<SimpleJSON.JSONBool, Int32>();
        dict.Add(jsonBool, val);

        Int32 hash1 = jsonBool.GetHashCode();
        Console.WriteLine("hashcode: " + hash1);
        Console.WriteLine("containsKey: " + dict.ContainsKey(jsonBool));
        Console.WriteLine("containsValue: " + dict.ContainsValue(val));

        jsonBool.AsBool = false; // mutate the object in the dict to lose it
        Int32 hash2 = jsonBool.GetHashCode();
        Console.WriteLine("hashcode: " + hash2);
        Console.WriteLine("containsKey: " + dict.ContainsKey(jsonBool));
        Console.WriteLine("containsValue: " + dict.ContainsValue(val));
    }
}

actual output:

hashcode: 1
containsKey: False
containsValue: True
hashcode: 0
containsKey: False
containsValue: True

Expected behavior

  • dict.ContainsKey(jsonBool) should return true immediately after dict.Add(jsonBool, val) when using the same instance as the lookup key.
  • x.Equals(x) equals true

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions