Xavier Lowmiller
Xavier Lowmiller

Reputation: 1391

Swift: Decode arbitrary protocol types

I'm trying to decode JSON that has a variable property which conforms to a protocol.

Consider the following set of structs:

protocol P: Decodable {
    var id: String { get }
}

struct A: P {
    let id: String
    var someThing: Double
}

struct B: P {
    let id: String
    var anotherThing: String
}

struct S: Decodable {
    let id: String
    let instanceOfProtocol: P
}

We're trying to decode S.

The automatic synthesis of Decodable does not work (because the decoder can't know which type P is going to be decoded to) so I'm trying to do this in a custom initializer:

Option 1: Exhaustively Check Conforming Types:

if let instance = try? container.decode(A.self, forKey: .instanceOfProtocol) {
    instanceOfProtocol = instance
} else if let instance = try? container.decode(B.self, forKey: .instanceOfProtocol) {
    instanceOfProtocol = instance
} else {
    throw NoConformingTypeError()
}

This works, but is very verbose, repetitive, and doesn't scale well, so I'm looking for other options.

Option 2: (Ab)use superDecoder:

let possibleTypes: [P.Type] = [A.self, B.self]
let childDecoder = try container.superDecoder(forKey: .instanceOfProtocol)
let decoded: [P] = possibleTypes.compactMap { try? $0.init(from: childDecoder) }
guard let instance = decoded.first else { throw NoConformingTypeError() }
instanceOfProtocol = instance

This works as well, but I'm not sure if superDecoder is meant to be used this way, or if it will break in the future.

Option 3:

let possibleTypes: [P.Type] = [A.self, B.self]
let decoded: [P] = possibleTypes.compactMap { try? container.decode($0, forKey: .instanceOfProtocol) }
guard let instance = decoded.first else { throw NoConformingTypeError() }
instanceOfProtocol = instance

This feels like the best option so far, but doesn't compile due to Ambiguous reference to member 'decode(_:forKey:)'.

Edit:

Option 4: Using a Generic Type:

struct S<T: P>: Decodable {
    let id: String
    let instanceOfProtocol: T
}

This is really nice, because synthesis of Decodable works again!

However, now we have to know what type T will be, because the decoding site now requires a type:

try JSONDecoder().decode(S<A>.self, from: data)
try JSONDecoder().decode(S<B>.self, from: data)

In our use case, we can't know what the type will be before, so we'd have to check here again...

Upvotes: 2

Views: 248

Answers (1)

Mojtaba Hosseini
Mojtaba Hosseini

Reputation: 119168

Use generic type:

struct S<T: P>: Decodable {
    let id: String
    let instanceOfProtocol: T
}

Remember Protocol is not a Type! And Swift is strongly typed language. So it MUST know the type of all objects at first place even though the actual type is not exposable to the caller of the object.

Upvotes: 2

Related Questions