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)