Dynamic equality checking, and Equatable

This topic has come up many, many times before. Here's one which shows some alternatives. I can't remember if one of the previous discussions includes an "official" answer for why Equatable is this way, but I assume one of them does. It's certainly a well-known limitation.

What you propose (OpaqueEquatable) is also a kind of type erasure. I'm not sure what you mean by "by hand", but you can largely automate it with a protocol requirement + default implementation (see below).

If it's just for testing, you could test against a known deserialised copy or serialise it, then compare the data/string versions.

Anyway, here's a modified version of your example which includes a simple kind of "polymorphic" equatable. If it's just for testing, you can modify the #if true lines to something meaningful (I don't think we have a compile-time conditional for @testable builds, but that might be worth adding...):

protocol JSONValue {
  #if true
  func isEqual(to: JSONValue) -> Bool
  #endif
}
extension Array: JSONValue where Element: JSONValue {}
extension Dictionary: JSONValue where Value: JSONValue {}
extension String: JSONValue {}
extension Date: JSONValue {}
extension Int: JSONValue {}
extension Bool: JSONValue {}
extension Double: JSONValue {}
extension NSNull: JSONValue {}
extension AnyHashable: JSONValue {}
extension NSArray: JSONValue {}
extension NSDictionary: JSONValue {}

#if true
extension JSONValue where Self: Equatable {
  func isEqual(to: JSONValue) -> Bool {
    guard let other = to as? Self else { return false }
    return self == other
  }
}

// Might be worth duplicating these for NSArray/NSDictionary as well.
// I didn't think through all the edge-cases...
extension Array where Element: JSONValue {
  func isEqual(to: JSONValue) -> Bool {
    guard let other = to as? Array<JSONValue>,
          count == other.count else { return false }
    return (0..<count).allSatisfy { self[$0].isEqual(to: other[$0]) }
  }
}
extension Dictionary where Value: JSONValue {
  func isEqual(to: JSONValue) -> Bool {
    guard let other = to as? Dictionary<Key, JSONValue>,
          count == other.count else { return false }
    return allSatisfy { (k, v) in other[k].map { v.isEqual(to: $0) } ?? false }
  }
}
#endif

A quick test:

let testJSON =
    """
    {
      "test": [ 1, 2, 3 ],
      "another": "99"
    }
    """
    let data = testJSON.data(using: .utf8)!

    let obj = try! JSONSerialization.jsonObject(with: data, options: [])
	let jsObj = obj as! JSONValue
    assert(jsObj.isEqual(to: jsObj))

    let obj2 = try! JSONSerialization.jsonObject(with: data, options: [])
    let jsObj2 = obj2 as! JSONValue
    assert(jsObj.isEqual(to: jsObj2))

    let testJSON2 =
    """
    {
      "test": [ 4, 5, 6 ],
      "another": "66"
    }
    """
    let data2 = testJSON2.data(using: .utf8)!
    let obj3 = try! JSONSerialization.jsonObject(with: data2, options: [])
    let jsObj3 = obj3 as! JSONValue
    assert(jsObj.isEqual(to: jsObj3) == false)
1 Like