1

So we have two types of JSON Responses in our API:

{
  "data": { } // some object in here
  "meta": { } // an object in here
}

and

{
  "data": [ ] // array of objects
  "meta": { } // an object in here
}

To decode them we use a JSONDecoder() and the following generic response struct:

public struct Response<T: Codable>: Codable {
    public let data: T
    public let meta: Meta?
}

This works fine with Swift 4.0 using .map(Response<[MyObject]>.self) or .map(Response<MySingleObject>.self)

but for some reason this doesn't work with Swift 4.1 and Xcode 9.3 anymore. It looks like it does not map "data" at all and therefor thinks the list of [MyObject] is on the first level.

dataCorrupted: Swift.DecodingError.Context
      ▿ codingPath: 3 elements
        - CodingKeys(stringValue: "data", intValue: nil)
        ▿ _JSONKey(stringValue: "Index 0", intValue: 0)
          - stringValue: "Index 0"
          ▿ intValue: Optional(0)
            - some: 0
        - CodingKeys(stringValue: "creationDate", intValue: nil)
      - debugDescription: "Date string does not match format expected by formatter."

Note that "creationDate" is a property of MyObject. The date format is definitely correct (set to .formatted(customFormatter) in the Decoder) as it works with Swift 4.0 in Xcode 9.2

How can we still have the same behaviour with Swift 4.1? The goal here was to not create a typed Response Type for each API Response but use a generic instead because the only difference is the response object type and that sometimes it returns a list and sometimes a single object underneath data.

Also related: Is there a way to enforce that if we use Response<[MyObject]>.self that also MyObject has to conform to Codable?

Thanks in advance.

Edit:

The code below maps correctly in Xcode 9.2 and Swift 4 but does not map (creates nil) in Xcode 9.3 and Swift 4.1

public struct MyObject: Codable {
    public let creationDate: Date

    enum CodingKeys: String, CodingKey {
        case creationDate = "creation_date"
    }
}


public struct Response<T: Codable>: Codable {
    public let data: T
    public let meta: Meta?
}


public struct Meta: Codable {
    public let count: Int?
}


let formatter = DateFormatter()
formatter.dateFormat = "yyyy-mm-dd HH:mm:ss"

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(formatter)

let jsonDataSingle: Data = """
    {
        "data": { "creation_date": "2018-04-29 18:00:11" },
        "meta": null
    }
""".data(using: .utf8)!

let jsonDataList: Data = """
    {
        "data": [{ "creation_date": "2018-04-10 17:00:11" }, { "creation_date": "2018-04-29 18:00:11" }],
        "meta": null
    }
""".data(using: .utf8)!

let singleObject = try? decoder.decode(Response<MyObject>.self, from: jsonDataSingle)
dump(singleObject)
let listOfObjects = try? decoder.decode(Response<[MyObject]>.self, from: jsonDataList)
dump(listOfObjects)
  • @matt I edit the post with sample code which works in Xcode 9.2 / Swift 4 but does not in Xcode 9.3 and Swift 4.1. If thats not the right solution, we appreciate suggestions how to handle this case better (without creating a response model for each endpoint :( ... ) thanks – Patrick Schneider Apr 29 '18 at 18:09
  • @matt This is just an example object with one property. MyObject could be various json objects or lists with objects from the API . Think: Response<[Product].self>, Response, Response<[Order].self>. I wonder how this would be mappable with Codable objects without a generic like Response. For the actual solved problem, see my comment on Price's answer below. – Patrick Schneider Apr 29 '18 at 19:25

1 Answers1

0

I don't get any errors. Please post it. As an exercise I added CustomStringConvertible conformance and replaced dump with print.

extension Response: CustomStringConvertible {
  public var description: String {
    return "data = \(data) | meta = \(meta)"
  }
}

extension MyObject: CustomStringConvertible {
  public var description: String {
    return "date = \(creationDate)"
  }
}

I am using Xcode 9.3, Swift 4.1 version 9E145 released through the App Store.

enter image description here

Price Ringo
  • 3,424
  • 1
  • 19
  • 33
  • So your answer got me to test that again. And this time my Phone wasn't attached and therefor it booted up the simulator. Turns out the actual problem here was that my phone runs on 12hr time format and DateFormatter overwrites the custom HH format. See https://stackoverflow.com/questions/2135267/nsdateformatter-with-24-hour-times for reference. Thank you for trying and proofing that the mapping is not the actual problem. – Patrick Schneider Apr 29 '18 at 19:23