Search code examples
iosjsonswift4codablejsondecoder

How to handle JSON format inconsistencies with Swift 4 Codable?


I need to parse JSON of which one of the field value is either an array:

"list" :
[
    {
        "value" : 1
    }
]

or an empty string, in case there's no data:

"list" : ""

Not nice, but I can't change the format.

I'm looking at converting my manual parsing, for which this was easy, to JSONDecoder and Codable struct's.

How can I handle this nasty inconsistency?


Solution

  • You need to try decoding it one way, and if that fails, decode it the other way. This means you can't use the compiler-generated decoding support. You have to do it by hand. If you want full error checking, do it like this:

    import Foundation
    
    struct ListItem: Decodable {
        var value: Int
    }
    
    struct MyResponse: Decodable {
    
        var list: [ListItem] = []
    
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            do {
                list = try container.decode([ListItem].self, forKey: .list)
            } catch {
                switch error {
                // In Swift 4, the expected type is [Any].self, but I think it will be [ListItem].self in a newer Swift with conditional conformance support.
                case DecodingError.typeMismatch(let expectedType, _) where expectedType == [Any].self || expectedType == [ListItem].self:
                    let dummyString = try container.decode(String.self, forKey: .list)
                    if dummyString != "" {
                        throw DecodingError.dataCorruptedError(forKey: .list, in: container, debugDescription: "Expected empty string but got \"\(dummyString)\"")
                    }
                    list = []
                default: throw error
                }
            }
        }
    
        enum CodingKeys: String, CodingKey {
            case list
        }
    
    }
    

    If you want no error checking, you can shorten init(from:) to this:

        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            list = (try? container.decode([ListItem].self, forKey: .list)) ?? []
        }
    

    Test 1:

    let jsonString1 = """
    {
        "list" : [ { "value" : 1 } ]
    }
    """
    print(try! JSONDecoder().decode(MyResponse.self, from: jsonString1.data(using: .utf8)!))
    

    Output 1:

    MyResponse(list: [__lldb_expr_82.ListItem(value: 1)])
    

    Test 2:

    let jsonString2 = """
    {
        "list" : ""
    }
    """
    print(try! JSONDecoder().decode(MyResponse.self, from: jsonString2.data(using: .utf8)!))
    

    Output 2:

    MyResponse(list: [])