Search code examples
swiftgenericscodabledecodablejsondecoder

Generic Decodable stopped working with swift 4.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)

Solution

  • 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