Search code examples
swiftxcodedecodable

How can I find out which element in the coding path exactly is causing a Swift Decoding error?


I'm calling an API endpoint getChatMessages and decode the response as follows:

func getChatMessages(conversation: Int, pagesize: Int = 10, page: Int = 1, completion: @escaping(Result<ConversationDetails, APIError>) -> Void) {
    
    {...some networking - configure request and start dataTask}
    
        do {
            let dateFormatter: DateFormatter = DateFormatter()
            dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
            let decoder = JSONDecoder()
            decoder.dateDecodingStrategy = .formatted(dateFormatter)
            decoder.keyDecodingStrategy = .custom { codingKeys in
                let lastKey = codingKeys.last!
                if lastKey.intValue != nil || codingKeys.count != 2 { return lastKey }
                if lastKey.stringValue == "count" || lastKey.stringValue == "conversation_participants" { return lastKey }
                return AnyCodingKey(stringValue: "element")!
            }
            let result = try decoder.decode(ResponseMultipleElements<ConversationDetails>.self, from: data!)
            completion(.success(result.detailresponse.element))
        }catch {
            print("Error is: ", error)
            completion(.failure(.decodingError))
        }
    }
    dataTask.resume()
}

This is ResponseMultipleElements structure

struct ResponseMultipleElements<T1: Decodable>: Decodable {
    let statuscode: Int
    let response_type: Int
    let errormessage: String?
    let detailresponse: Element<T1>

}

struct Element<T2: Decodable>: Decodable {
    let count: Int
    let element: T2
    let conversation_participants: [ConversationParticipants]?
}

struct AnyCodingKey: CodingKey {

    var stringValue: String
    init?(stringValue: String) {
        self.stringValue = stringValue
    }

    var intValue: Int? { return nil }
    init?(intValue: Int) {
        return nil
    }
}

And finally, the structure of a ConversationDetails object, as returned by my getChatMessages function

class ConversationDetails: Codable {
    
    var messages: [ChatMessage]?
    var conversation_participants: [ConversationParticipants]?
}

Here's how the response from the getChatMessages API call looks like: enter image description here

PROBLEM: I'm getting typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "detailresponse", intValue: nil), CodingKeys(stringValue: "element", intValue: nil)], debugDescription: "Expected to decode Dictionary<String, Any> but found an array instead.", underlyingError: nil))

Went over everything numerous times now but can't find the error. I'm not sure for which element it finds an array instead of the expected <String, Any> so don't know what to fix. Hoping someone can help.


Solution

  • Your keyDecodingStrategy tells the decoder to treat all the string keys that have exactly one parent, that is not count or conversation_participants, as "element". There is only one such key in your JSON that satisfy this condition - messages, under detailresponse.

    The messages key would be decoded by:

    let element: T2
    

    where T2 is:

    class ConversationDetails: Codable {
        var messages: [ChatMessage]?
        var conversation_participants: [ConversationParticipants]?
    }
    

    But messages is actually an array in the JSON!

    What I suggest you do is to decode a ResponseMultipleElements<[ChatMessage]?>. This way, T2 is actually the expected array type.

    You should add an initialiser to ConversationDetails to allow you to create a ConversationDetails from an array of chat messages and participants,

    init(messages: [ChatMessage]?, conversation_participants: [ConversationParticipants]?) {
        self.messages = messages
        self.conversation_participants = conversation_participants
    }
    

    and use it in getChatMessages:

    let result = try decoder.decode(ResponseMultipleElements<[ChatMessage]?>.self, from: data!)
    let convoDetails = ConversationDetails(
        messages: result.detailresponse.element, 
        conversation_participants: result.detailresponse.conversation_participants
        )
    completion(.success(convoDetails))