skip to Main Content

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?

2

Answers


  1. 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
    }
    
    Login or Signup to reply.
  2. 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search