Protocol extension only for certain types of dictionaries?

Hello,


I'm trying to add JSON compliance to native Swift data types in a super lightweight fashion (possibly without adding another abstraction layer, which is useful for NSJSONSerialization etc.). Consider the following code:


protocol JSONValueType {}

extension String: JSONValueType {}
extension Int: JSONValueType {}
extension Double: JSONValueType {}
extension Bool: JSONValueType {}
extension NSNull: JSONValueType {}

// This method demonstrates that the approach works
func doWithJSONValue(jsonValue: JSONValueType) {
    // ...
}

doWithJSONValue("Hello")
doWithJSONValue(100)
doWithJSONValue(3.1415)
doWithJSONValue(true)
doWithJSONValue(NSNull())


This works and allows me to mark certain data types as JSON value types, which in turn allows me to write functions that only accept those types. Now valid JSON requires an array or a dictionary (with strings as keys) as top level element (and of course they can be nested too). So I tried to add another protocol extension to those data types as well:


protocol JSON {}

// This is what I actually want
func doWithJSON(json: JSON) {
    // ...
}

// I'd start with the following, but
extension Dictionary where Key: String, Value: JSONValueType: JSON {}


However this is not valid Swift 2 code. I'm wondering if I'm just doing it wrong or if there is another simple solution that I did not yet think of.

You have to predefine the protocol you're asking for:

protocol JSONValueTypeJSON: JSONValueType, JSON {}
extension Dictionary where Key: String, Value: JSONValueTypeJSON {}

Thank you, maybe I'm missing something but I'm afraid I don't see how this helps. I want to abuse the extension mechanism to annotate all dictionary types as JSON whose key is a String and whose value conforms to JSONValueType or JSON – just like I did with String, Int, Double etc. which I annotated with JSONValueType as can be seen here:


"Hello" is JSONValueType // returns true
100 is JSONValueType // returns true
true is JSONValueType // returns true
Float(5) is JSONValueType // returns false because I did not extend Float to conform to JSONValueType


I followed your suggestion and added


protocol JSON {}
protocol JSONValueTypeJSON: JSONValueType, JSON {}
extension Dictionary where Key: CustomStringConvertible, Value: JSONValueTypeJSON {}


I would now expect

["a": "b"] is JSON

or

["a": "b"] is JSONValueTypeJSON

to be true, but it is still false. I guess what happens is that I address certain dictionary types and then... basically do nothing with them, in particular they don't adopt the JSON protocol. What I'd like to know is, if possible at all, how to add protocol adoption via extensions to types with a "where" constraint.

Accepted Answer

As you discovered it is not (yet) possible to conform to a protocol with a constrained extension.


Depending on your situation the following could be a decent workaround:

func doWithJSON<V: JSONValueType>(json: Dictionary<String,V>) {
}

func doWithJSON<V: JSONValueType>(json: [V]) {
}

Sorry, I could not tell that "JSON" wasn't attached to Value itself. I saw:

Value: JSONValueType: JSON


Don't you just want a typealias? Is anything else going to be JSON?

typealias JSON = [String: JSONValueType]


Edit: Nevermind, I see now that you also wanted [JSONValueType] to be JSON.

Unfortunately this solution doesn't allow for nested (recursive) JSON data, but thanks anyway! My previous approach was a specific JSON data structure:


protocol JSONValue {}

extension String: JSONValue {}
extension Int: JSONValue {}
extension Double: JSONValue {}
extension Bool: JSONValue {}
extension NSNull: JSONValue {}

indirect enum JSON {
    case Collection([JSONInternal])
    case Object([String: JSONInternal])
}

indirect enum JSONInternal {
    case Collection([JSONInternal])
    case Object([String: JSONInternal])
    case Value(JSONValue)
}

let json: JSON = .Collection([.Value(3), .Value(true), .Object(["key": .Value("value")])])


but that's obviously very messy. My hope was I could elegantly "extract" the JSON equivalent subset of native Swift data types, but that seems not to be an option for now.


Are there any other suggestions?

I see where you're coming from, sorry for not being clear enough.

The following would be a minor improvement but obviously doesn't solve your actual issue:

// ...
indirect enum JSON : JSONValue {
    case Collection([JSONValue])
    case Object([String : JSONValue])
}
let json: JSON = .Collection([3, true, JSON.Object(["key" : "value"])])


Ultimately what you wan't to do unfortunately just isn't possible in current day Swift without some kind of kludgy workaround.


That being said, if you have a more specific goal beyond this particular issue (like serialization; you mentioned NSJSONSerialization) someone might be able to propose alternative approaches...

That's actually really clever as it reduces both, complexity and verbosity. I'll definitely use this as fallback solution.


Even so, this structure adds a data layer to my app that feels unnecessary. I'm currently working on client/server communication which heavily relies on the exchange of non fixed formatted JSON data. Unsurprisingly I use NSJSONSerialization nearly everywhere, which means that at code level the data is readily available in the form of native Swift data types. This is great as it keeps my implementation flat and simple, it's just that the data happens to be JSON by convention rather than by restriction. That's ok when the data comes from the server. But in the case of uploading I want my API to be safe against non-JSON data (i.e. my methods should "accept anything as long as it is JSON") without adding too much overhead.


Edit: I also tried to do runtime JSON verification:


public func isJSON(data: AnyObject) -> Bool {
    if data is NSNumber {
        return true
    } else if data is String {
        return true
    } else if data is NSNull {
        return true
    } else if let object = data as? [String: AnyObject] {
        for value in object.values {
            if !isJSON(value) {
                return false
            }
        }
       
        return true
    } else if let array = data as? [AnyObject] {
        for element in array {
            if !isJSON(element) {
                return false
            }
        }
       
        return true
    } else {
        return false
    }
}


but I find this approach somewhat cumbersome as it adds so much boilerplate: for instance each time I mutate a "JSON instance variable" its new value must be verified before it can be set, for every such variable. I'd prefer to make this compile-time decidable if possible.

EDIT: Apparently including any link still results in the post getting stuck in moderation, so here is take 2:


Yeah, with non-formatted data you're probably not going to get around this issue.

You'd either have to give up static type safety and instead check at runtime or alternatively end up with some wrapper like above...


It's probably worth it to look at some of the Swift JSON libraries out there if you haven't already: (Just search GitHub for "swift json" and you'll get plenty results)

I haven't really used any of them myself and I doubt they can get around this issue in a fundamental way, they're probably nicer to use than NSJSONSerialization which is very Objective-C oriented.


Alternatively if you decide to stick with the simple wrapper above, it's probably worth it to add some convenience functionality yourself although there's obviously a limit to what you can achieve that way...

// For example add initializers to JSON to make wrapper creation a bit easier and more uniform
indirect enum JSON : JSONValue {
    case Collection([JSONValue])
    case Object([String : JSONValue])

    init(_ array: [JSONValue]) {
        self = .Collection(array)
    }

    init(_ dictionary: [String : JSONValue]) {
        self = .Object(dictionary)
    }
}

// Or if you want to work with JSONValue?'s
extension Optional where Wrapped : JSONValue {
    func toJSON() -> JSONValue {
        switch self {
        case .None:
            return NSNull()
        case .Some(let wrapped):
            return wrapped
        }
    }
}

// Depending on how you handle things, you might be able to use JSON internally most of the time
// and convert from AnyObject at boundaries. In that case directly converting might be more useful
// than just checking validity...
func toJSONValue(data: AnyObject) -> JSONValue? {
    switch data {
    case let number as NSNumber:
        let type = UnicodeScalar(UInt8(number.objCType.memory))
        switch type {
        case "f", "d":
            let value = number.doubleValue
            guard !value.isInfinite && !value.isNaN else { return nil }
            return value
        default:
            return number.integerValue
        }
       
    case let string as String:
        return string
    case is NSNull:
        return NSNull()
    case let object as [String: AnyObject]:
        var json: [String : JSONValue] = Dictionary(minimumCapacity: object.count)
       
        for (key, value) in object {
            if let jsonValue = toJSONValue(value) {
                json[key] = jsonValue
            } else {
                return nil
            }
        }
        return JSON(json)
    case let array as [AnyObject]:
        var json: [JSONValue] = []
        json.reserveCapacity(array.count)
        for value in array {
            if let jsonValue = toJSONValue(value) {
                json.append(jsonValue)
            } else {
                return nil
            }
        }
        return JSON(json)
    default: return nil
    }
}

extension JSON {
    init?(_ data: AnyObject) {
        switch toJSONValue(data) {
        case let json as JSON:
            self = json
        default:
            return nil
        }
    }
}

Thank you very much! I think I'll need to reevaluate my priorities here and eventually use one of the available libraries, at least for now.


Edit: Actually, I might continue working on the JSON struct as a little hobby project and use your suggestions as starting point 😉

Protocol extension only for certain types of dictionaries?
 
 
Q