Home > Enterprise >  Conforming a protocol to codable in extension
Conforming a protocol to codable in extension

Time:12-21

There are a number of questions on this topic but I haven't figured out why my solution doesn't work yet.

I have some protocol

protocol Foo: Decodable {
    var prop1: String? { get set }
    var prop2: Bool? { get set }
    var prop3: Int? { get set }

    init()
}

enum FooCodingKeys: CodingKey { case prop1, prop2, prop3 }
extension Foo {
    init(from decoder: Decoder) throws {
        self.init()

        let container = try decoder.container(keyedBy: FooCodingKeys.self)
        self.prop1 = try container.decode(String?, forKey: .prop1)
        self.prop2 = try container.decode(Bool?, forKey: .prop2)
        self.prop3 = try container.decode(Int?, forKey: .prop3)
    }

}

Technically, this now has a default implementation which should make the whole protocol perfectly decodable. And the compiler doesn't complain about this at all. So now in a struct, If I have

enum BarCodingKeys: CodingKey { case foos }
struct Bar: Decodable {
    var foos: [Foo]

    init(from decoder: Decoder) {
        let container = try decoder.container(keyedBy: BarCodingKeys.self)
        self.foos = try container.decode([Foo].self, forKey: .prop1)
    }
}

Then I get the error Protocol 'Foo' as a type cannot conform to 'Decodable'.

Is there a way for me to make protocol's conform to Codable using extensions? and if not, why?

CodePudding user response:

I'm not sure what your use case is, so what I propose may not fit, but easiest way out is to tell compiler that you are decoding a concrete type, not a protocol. But that concrete type implements Foo. So you change Bar like this:

struct Bar<T: Foo>: Decodable {
    
    var foos: [T]

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: BarCodingKeys.self)
        self.foos = try container.decode([T].self, forKey: .foos)
    }
}

So now you are decoding [T].self - a concrete type, not a protocol. Of course the drawback is that you have to provide a type when you are decoding the class itself, i.e. you cannot say:

JSONDecoder().decode(Bar.self, from: jsonData)

You have to provide a type here:

struct Foo1: Foo {
    
    var prop1: String?
    var prop2: Bool?
    var prop3: Int?
}

let a = try JSONDecoder().decode(Bar<Foo1>.self, from: jsonData)

However, as you see Foo1 does not need to implement init(from decoder: Decoder) throws, the one from protocol is correctly used.

Additional note: as you may noticed I changed forKey: .prop1 to forKey: .foos, because based on your code you expect an object with property foos, which as a value contains an array of objects that match protocol Foo, something like this:

{ "foos": [
      { "prop1": ...,
        "prop2": ...,
        "prop3": ...
      },
      { "prop1": ...,
        "prop2": ...,
        "prop3": ...
      },
      ...
  ]
}

If this is not the case, please provide an example of JSON you are trying to decode. And also you need to fix this function (use decodeIfPresent instead of optional):

extension Foo {
    init(from decoder: Decoder) throws {
        self.init()

        let container = try decoder.container(keyedBy: FooCodingKeys.self)
        self.prop1 = try container.decodeIfPresent(String.self, forKey: .prop1)
        self.prop2 = try container.decodeIfPresent(Bool.self, forKey: .prop2)
        self.prop3 = try container.decodeIfPresent(Int.self, forKey: .prop3)
    }
}

CodePudding user response:

First, I think there's a key misunderstanding here:

protocol Foo: Decodable { ... }

In your subject, you suggest this is "conforming a protocol to Codable," but that's not what this does. This says "in order to conform to Foo, a type must first conform to Decodable." Foo absolutely does not itself conform to Codable. Then you note:

Technically, this now has a default implementation which should make the whole protocol perfectly decodable.

This absolutely is not true in either direction. Consider a simple example:

struct ConcreteFoo: Foo {
    var prop1: String?
    var prop2: Bool?
    var prop3: Int?
    var anotherPropNotInFoo: CBPeripheral
 }

How does your init(from:) decode ConcreteFoo? What value is applied to anotherPropNotInFoo?

In the other direction, consider three conforming types:

struct FooA: Foo {
    var prop1: String?
    var prop2: Bool?
    var prop3: Int?
    var anotherProp: Bool = true
}

struct FooA: Foo {
    var prop1: String?
    var prop2: Bool?
    var prop3: Int?
}

struct FooC: Foo {
    var prop1: String?
    var prop2: Bool?
    var prop3: Int?
    var anotherProp: Bool = true
    func doC() { print("I'm a C!") }
}

Say your JSON were:

[
    {},
    { "anotherProp": false }
]

Now consider this code:

let bar = try JSONDecoder().decode(Bar.self, from: json)
for foo in bar.foos {
    print("\(type(of: foo)")
    if let c = foo as? FooC {
        c.cThing()
    }
}

What should happen? What actual type should these be? Remember there are an unbounded number of other implementations of Foo that might exist (including in other modules). There's no way for this to work. If you mean that Foo only has precisely these properties, no others, and no other methods, it's not a protocol. It's just a struct. If you mean there to be an unbounded number of types that implement it, then there's no way to determine what they are.

  • Related