3

I'm currently trying to decode JSON which looks like this:

{
  "result": {
    "success": true,
    "items": [
      {
        "timeEntryID": "1",
        "start": "1519558200",
        "end": "1519563600",
        "customerName": "Test-Customer",
        "projectName": "Test-Project",
        "description": "Entry 1",
      },
      {
        "timeEntryID": "2",
        "start": "1519558200",
        "end": "1519563600",
        "customerName": "Test-Customer",
        "projectName": "Test-Project",
        "description": "Entry 2",
      }
    ],
    "total": "2"
  },
  "id": "1"
}

The decoding process for this specific type of JSON is pretty simple. I just need something like this:

struct ResponseKeys: Decodable {
    let result: ResultKeys

    struct ResultKeys: Decodable {
        let success: Bool
        let items: [Item]
    }
}

Now the problem I'm facing is that every response of the server has the same structure as the above JSON but with different item types. So sometimes it is let items: [Item] but it could also be let items: [User] if I make a call to the User endpoint.

Because it would be an unnecessary duplication of code if I would write the above swift code for every endpoint with just the modification of the items array, I created a custom decoder:

enum KimaiAPIResponseKeys: String, CodingKey {
    case result

    enum KimaiResultKeys: String, CodingKey {
        case success
        case items
    }
}

struct Activity: Codable {
    let id: Int
    let description: String?
    let customerName: String
    let projectName: String
    let startDateTime: Date
    let endDateTime: Date

    enum CodingKeys: String, CodingKey {
        case id = "timeEntryID"
        case description
        case customerName
        case projectName
        case startDateTime = "start"
        case endDateTime = "end"
    }
}

extension Activity {

    init(from decoder: Decoder) throws {
        let resultContainer = try decoder.container(keyedBy: KimaiAPIResponseKeys.self)
        let itemsContainer = try resultContainer.nestedContainer(keyedBy: KimaiAPIResponseKeys.KimaiResultKeys.self, forKey: .result)
        let activityContainer = try itemsContainer.nestedContainer(keyedBy: Activity.CodingKeys.self, forKey: .items)

        id = Int(try activityContainer.decode(String.self, forKey: .id))!
        description = try activityContainer.decodeIfPresent(String.self, forKey: .description)
        customerName = try activityContainer.decode(String.self, forKey: .customerName)
        projectName = try activityContainer.decode(String.self, forKey: .projectName)
        startDateTime = Date(timeIntervalSince1970: Double(try activityContainer.decode(String.self, forKey: .startDateTime))!)
        endDateTime = Date(timeIntervalSince1970: Double(try activityContainer.decode(String.self, forKey: .endDateTime))!)
    }

}

The decoder works perfectly if "items" does only contain a single object and not an array:

{
  "result": {
    "success": true,
    "items":
      {
        "timeEntryID": "2",
        "start": "1519558200",
        "end": "1519563600",
        "customerName": "Test-Customer",
        "projectName": "Test-Project",
        "description": "Entry 2",
      },
    "total": "2"
  },
  "id": "1"
}

If items is an array I get the following error:

typeMismatch(Swift.Dictionary, Swift.DecodingError.Context(codingPath: [__lldb_expr_151.KimaiAPIResponseKeys.result], debugDescription: "Expected to decode Dictionary but found an array instead.", underlyingError: nil))

I just cannot figure out how to modify my decoder to work with an array of items. I created a Playground file with the working and not working version of the JSON. Please take a look and try it out: Decodable.playground

Thank you for your help!

2 Answers 2

3

My suggestion is to decode the dictionary/dictionaries for items separately

struct Item : Decodable {

    enum CodingKeys: String, CodingKey {
        case id = "timeEntryID"
        case description, customerName, projectName
        case startDateTime = "start"
        case endDateTime = "end"
    }

    let id: Int
    let startDateTime: Date
    let endDateTime: Date
    let customerName: String
    let projectName: String
    let description: String?

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = Int(try container.decode(String.self, forKey: .id))!
        description = try container.decodeIfPresent(String.self, forKey: .description)
        customerName = try container.decode(String.self, forKey: .customerName)
        projectName = try container.decode(String.self, forKey: .projectName)
        startDateTime = Date(timeIntervalSince1970: Double(try container.decode(String.self, forKey: .startDateTime))!)
        endDateTime = Date(timeIntervalSince1970: Double(try container.decode(String.self, forKey: .endDateTime))!)
    }
}

And in Activity use a conditional initializer, it provides it's own do catch block. First it tries to decode a single item and assigns the single item as array to the property. If it fails it decodes an array.

enum KimaiAPIResponseKeys: String, CodingKey {
    case result, id

    enum KimaiResultKeys: String, CodingKey {
        case success
        case items
    }
}

struct Activity: Decodable {
    let id: String
    let items: [Item]
}

extension Activity {

    init(from decoder: Decoder) throws {
        let rootContainer = try decoder.container(keyedBy: KimaiAPIResponseKeys.self)
        id = try rootContainer.decode(String.self, forKey: .id)
        let resultContainer = try rootContainer.nestedContainer(keyedBy: KimaiAPIResponseKeys.KimaiResultKeys.self, forKey: .result)
        do {
            let item = try resultContainer.decode(Item.self, forKey: .items)
            items = [item]
        } catch {
            items = try resultContainer.decode([Item].self, forKey: .items)
        }
    }
} 
Sign up to request clarification or add additional context in comments.

3 Comments

Thanks for your quick answer! I think you are right that I need to write a custom decoder for the items separately. If I would implement your answer than I could decode JSON which has a single Item or an array of Items but I always have an array which can contain different types. eg User/Activity. "Item" was just a generic term for these types. How could I accomplish this behavior since I need to specify the type in "let items: [here]"
Just make Activity generic: struct Activity<Item: Decodable>, and it will work the same.
@RobNapier That was the decisive point! So simple. Thank you and vadian. I really appreciate your helpful answers!
2

You Can Use Generics, It's a neat way to deal with this situation.

  struct MainClass<T: Codable>: Codable  {
     let result: Result<T>
     let id: String
  }

  struct Result <T: Codable>: Codable {
     let success: Bool
     let items: [T]
     let total: String
  }

and here you will get the items

   let data = Data()
   let decoder = JSONDecoder()
   let modelObjet = try! decoder.decode(MainClass<User>.self, from: data)
   let users = modelObjet.result.items

In my opinion, Generics is the best way to handle the duplication of code like this situations.

1 Comment

This is exactly how I did it. Generics rock!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.