Home > OS >  Swift Codable struct with a generic property
Swift Codable struct with a generic property

Time:11-26

Say we've got a cursor based paginated API where multiple endpoints can be paginated. The response of such an endpoint is always as follows:

{
    "nextCursor": "someString",
    "PAYLOAD_KEY": <generic response>

}

So the payload always returns a cursor and the payload key depends on the actual endpoint we use. For example if we have GET /users it might be users and the value of the key be an array of objects or we could cal a GET /some-large-object and the key being item and the payload be an object.
Bottom line the response is always an object with a cursor and one other key and it's associated value.

Trying to make this generic in Swift I was thinking of this:

public struct Paginable<Body>: Codable where Body: Codable {
    public let body: Body
    public let cursor: String?

    private enum CodingKeys: String, CodingKey {
        case body, cursor
    }
}

Now the only issue with this code is that it expects the Body to be accessible under the "body" key which isn't the case.

We could have a struct User: Codable and the paginable specialized as Paginable<[Users]> where the API response object would have the key users for the array.

My question is how can I make this generic Paginable struct work so that I can specify the JSON payload key from the Body type?

CodePudding user response:

The simplest solution I can think of is to let the decoded Body to give you the decoding key:

protocol PaginableBody: Codable {
    static var decodingKey: String { get }
}

struct RawCodingKey: CodingKey, Equatable {
    let stringValue: String
    let intValue: Int?

    init(stringValue: String) {
        self.stringValue = stringValue
        intValue = nil
    }

    init(intValue: Int) {
        stringValue = "\(intValue)"
        self.intValue = intValue
    }
}

struct Paginable<Body: PaginableBody>: Codable {
    public let body: Body
    public let cursor: String?

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: RawCodingKey.self)
        body = try container.decode(Body.self, forKey: RawCodingKey(stringValue: Body.decodingKey))
        cursor = try container.decodeIfPresent(String.self, forKey: RawCodingKey(stringValue: "nextCursor"))
    }
}

For example:

let jsonString = """
{
    "nextCursor": "someString",
    "PAYLOAD_KEY": {}
}
"""
let jsonData = Data(jsonString.utf8)

struct SomeBody: PaginableBody {
    static let decodingKey = "PAYLOAD_KEY"
}

let decoder = JSONDecoder()
let decoded = try? decoder.decode(Paginable<SomeBody>.self, from: jsonData)
print(decoded)

Another option is to always take the "other" non-cursor key as the body:

struct Paginable<Body: Codable>: Codable {
    public let body: Body
    public let cursor: String?

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: RawCodingKey.self)

        let cursorKey = RawCodingKey(stringValue: "nextCursor")

        cursor = try container.decodeIfPresent(String.self, forKey: cursorKey)

        // ! should be replaced with proper decoding error thrown
        let bodyKey = container.allKeys.first { $0 != cursorKey }!
        body = try container.decode(Body.self, forKey: bodyKey)
    }
}

Another possible option is to pass the decoding key directly to JSONDecoder inside userInfo and then access it inside init(from:). That would give you the biggest flexibility but you would have to specify it always during decoding.

CodePudding user response:

You can use generic model with type erasing, for example

struct GenericInfo: Encodable {

  init<T: Encodable>(name: String, params: T) {
      valueEncoder = {
        var container = $0.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: . name)
        try container.encode(params, forKey: .params)
      }
   }

   // MARK: Public

   func encode(to encoder: Encoder) throws {
       try valueEncoder(encoder)
   }

   // MARK: Internal

   enum CodingKeys: String, CodingKey {
       case name
       case params
   }

   let valueEncoder: (Encoder) throws -> Void
}
  • Related