Robert Robert -4 years ago 73
Swift Question

How to know which type to instantiate from something like JSON?

My app loads a JSON file at launch. The JSON file includes instructions to create various instances of different types. As a basic example, I have a protocol called

BasicType
with some extensions to make some built-in Swift types conform to it:

protocol BasicType {

}

extension Int: BasicType { }

extension Float: BasicType { }

extension String: BasicType { }


I also have a dictionary which maps these type 'names' (as found in the JSON) to the types themselves:

let basicTypes = [
"integer": Int.self,
"float": Float.self,
"string": String.self
] as [String : BasicType.Type]


When I'm loading
BasicType
s from the JSON, I look up the name specified by the JSON using the dictionary above to know which type to instantiate (in reality, the protocol also defines initialisers, so this is possible). I have some other protocols in addition to
BasicType
which work in exactly the same way, and each one gets its own dictionary map.

For example, my JSON might include an array of
BasicType
s I want to instantiate:

{
"basic_types": [{
"type": "integer",
...
}, {
"type": "integer",
...
}, {
"type": "string",
...
}, {
"type": "float",
...
}]
}


The ellipses denote other attributes which are passed to the initialiser, like what value the integer should be. In actuality, these are custom structs and classes which take various properties. In my Swift code, I open up the
basic_types
array, look at the
type
key for each JSON object, look up the appropriate types and initialise them. So my loaded array would include two
Int
s, a
String
and a
Float
.

This is similar to how UIKit instantiates views from a storyboard, for example. The view's class name is stored in the storyboard, and in this case presumably it uses something like
NSClassFromString
to perform the mapping. I don't want to rely on the Objective-C runtime though.




The problem with this approach is that it's difficult to look up the name of a type if I already have the type or instance, as I have to inefficiently iterate the dictionary to search.

Instead, I thought a better solution might be to include each type's name (or 'type identifier') statically as a variable in the type itself, and then generate the type maps from that. So, I created a new protocol:

protocol TypeIdentifiable {
static var typeIdentifier: String { get }
}


And made
BasicType
(and all the other similar protocols) inherit from it:

protocol BasicType: TypeIdentifiable {

}


Which means I need to provide the names directly in the types themselves:

extension Int: BasicType {
static let typeIdentifier = "integer"
}

extension Float: BasicType {
static let typeIdentifier = "float"
}

extension String: BasicType {
static let typeIdentifier = "string"
}


I made a function (which compiles) which I intended to generically take a protocol conforming to
TypeIdentifiable
and including an array of types, and produce a dictionary containing the mapping:

func typeMap<T : TypeIdentifiable>(_ types: [T.Type]) -> [String : T.Type] {
return Dictionary(uniqueKeysWithValues: types.map { type in
(key: type.typeIdentifier, value: type)
})
}


I can then replace the dictionary literal with:

let basicTypes = typeMap([
Int.self,
Float.self,
String.self
] as [BasicType.Type])


This way, if I have a type I can easily get its name by accessing its static property, and if I have the name I can get the type through the generated dictionary.

Unfortunately, this doesn't work:


error: cannot convert value of type '[BasicType.Type]' to expected argument type '[_.Type]'


I think using a protocol as a generic type is forbidden in Swift, which probably makes it impossible to do what I'm trying to do.

My question is: Is there a way of doing what I'm trying to do? Otherwise, on a more general level, is there a better way of knowing what types to instantiate from something like JSON?

Answer Source

This is SR-3038 and is roughly "as designed." Protocols do not conform to themselves, so they cannot fulfill generic requirements. BasicType cannot be T because BasicType is a protocol. Why? Well, imagine I've written this code (which I expect is really similar to what you want to write):

protocol P {
    init()
}

func makeOne<T:P>(what: T.Type) -> T {
    return T.init()
}

Awesome. That's legal. Now, if protocols conformed to themselves, I could write this code:

makeOne(what: P.self)

What init should that call? Swift fixes this impasse by only allowing concrete types to conform to protocols. Since BasicType does not conform to BasicType, it also does not conform to TypeIdentifiable, and cannot be T.

So what do we do?

  • First and most important-top-of-the-list: Do you actually need this to be generic? How many times do you actually plan to use it for radically different JSON formats? Could you just write the parsing code directly (ideally with the new Codeable)? The tears that have been spilled trying to write complex JSON parsers to parse simple JSON formats could flood WWDC. My rule of thumb is to avoid making things generic unless I already know at least 3 different ways it will be used. (If that happens later, I refactor to generic when I have the examples.) You just don't know enough about the problem until have have several concrete examples and your generic solution will probably be wrong anyway.

  • OK, let's assume this really is a very general JSON format you have to parse where you have no idea what's going to be in it (maybe like the NIB format you mention). The first and most obvious answer: Use your existing solution. Your concern is that it's hard to look them up backwards (type->identifier), but that's trivial to write a method to make that easy. Unless you have many thousands, possibly millions, of types, the difference in time is beyond trivial; linear search can even be faster than dictionaries for small collections.

  • Next in line is to just write typeMap for each protocol (i.e. explicitly for BasicType rather than making it generic. There are several cases in stdlib where they do this. Sometimes you just have to do that.

  • And of course you could create a TypeDefinition struct that holds the info (like a type eraser). In practice this really just moves the problem around. Eventually you'll need to duplicate some code if there are a bunch of protocols.

But almost always when I head down this kind of road, I discover it was way over-engineered and just writing the silly code directly is the easy and scalable solution.

And of course look carefully at the new JSONDecoder in Swift 4. You want to be moving in that direction if you can anyway.

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download