6
votes

I have 2 protocols, Filters and Parameters, both of which extend Encodable

protocol Filters: Encodable {
    var page: Int { get }
}

protocol Parameters: Encodable {
    var type: String { get }
    var filters: Filters { get }
}

I create structs conforming to these protocols, thusly…

struct BankAccountFilters: Filters {
    var page: Int
    var isWithdrawal: Bool
}

struct BankAccountParamters: Parameters {
    let type: String = "Bank"
    var filters: Filters
}

let baf = BankAccountFilters(page: 1, isWithdrawal: true)
let bap = BankAccountParamters(filters: baf)

Which fails because…

error: type 'BankAccountParamters' does not conform to protocol 'Encodable'

note: cannot automatically synthesize 'Encodable' because 'Filters' does not conform to 'Encodable'

Filters clearly does conform to Encodable (at least it seems that way to me). Is there a way around this?

3
This looks like the same problem as in Protocol doesn't conform to itself?: A protocol does not conform to itself (or to any protocol that it inherits from).Martin R
It says in the docs that you are required to implement encode(to:): developer.apple.com/documentation/swift/encodableoyvindhauge

3 Answers

10
votes

As discussed in Protocol doesn't conform to itself?, a protocol does not conform to itself, or to a protocol that it inherits from. In your case, Filters does not conform to Encodable.

A possible solution is to make struct BankAccountParamters and protocol Parameters generic:

protocol Filters: Encodable {
    var page: Int { get }
}

protocol Parameters: Encodable {
    associatedtype T: Filters
    var type: String { get }
    var filters: T { get }
}

struct BankAccountFilters: Filters {
    var page: Int
    var isWithdrawal: Bool
}

struct BankAccountParamters<T: Filters>: Parameters {
    let type: String = "Bank"
    var filters: T
}

Now var filters has type T, which conforms to Filters and consequently, to Encodable.

This compiles and produces the expected result:

let baf = BankAccountFilters(page: 1, isWithdrawal: true)
let bap = BankAccountParamters(filters: baf)

let data = try! JSONEncoder().encode(bap)
print(String(data: data, encoding: .utf8)!)
// {"type":"Bank","filters":{"isWithdrawal":true,"page":1}}
4
votes

You cannot have protocol reference in the struct as the compiler will not be able to know the type at the time of encoding. Here is the bug reported SR-5853.

What you can do is create a type erasure for your protocol and use the erasure in place of protocol.

Something like this:

Update: As @MartinR answered there is no need of type erasure here.

protocol Filters: Encodable {
    var page: Int { get }
}

protocol Parameters: Encodable {
    associatedtype T: Filters
    var type: String { get }
    var filters: T { get }
}

struct BankAccountFilters: Filters {
    var page: Int
    var isWithdrawal: Bool
}

struct BankAccountParamters<T: Filters>: Parameters {
    let type: String = "Bank"
    var filters: T
}

let baf = BankAccountFilters(page: 1, isWithdrawal: true)
let bap = BankAccountParamters(filters: baf)

let encoder = JSONEncoder()
let data = try! encoder.encode(bap)
print(String(data: data, encoding: .utf8)!)

Here you will get the output:

{"type":"Bank","filters":{"isWithdrawal":true,"page":1}}
0
votes

To get what you want without using generics, because of the multiple problems that come with using generics all over your code, you can provide your own implementation of encode method for the struct BankAccountParameters.

extension BankAccountParamters {
  enum CodingKeys: String, CodingKey {
    case type, filters
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(type, forKey: .type)
    if let bankAccountFilters = filters as? BankAccountFilters {
      try container.encode(bankAccountFilters, forKey: .filters)
    } else {
      assertionFailure("Type conforming to Filters is not encoded properly")
  }
}

This way, your type need not have generic constraints. But the flip side is that you have to ensure that all types which you conform to Filters in the future are encoded properly, that is where the assertionFailure on the else block might help.

Similarly, if you are decoding the BankAccountParameters struct, you have to provide your custom implmentation of init(from: Decoder).