Skip to content

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

@udlose

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

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