3

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?

meaning-matters
  • 21,929
  • 10
  • 82
  • 142
  • I agree, you should give a dope slap to whomever created that web service. They should, at the very least, return `"list": null` if there's no list, rather than changing the type of the results! But, in answer to your question, you will have to write an `init(from:) throws` method that manually tries decoding `list`, gracefully handling when it is a string. See `init(from:)` example [Encoding and Decoding Custom Types](https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types). – Rob Feb 24 '18 at 20:30
  • 1
    If your *manual parsing* works, don't change it. The magic of `Codable` (adopt the protocol and you are done) happens only if the JSON is consistent. – vadian Feb 24 '18 at 20:30
  • 1
    @Rob It's only the New York Times API. – meaning-matters Feb 24 '18 at 20:31
  • Lol. It's still wrong. It deserves a bug report. Out of curiosity, which endpoint are you using? – Rob Feb 24 '18 at 20:50
  • @Rob Endpoint: https://api.nytimes.com/svc/mostpopular/v2/mostviewed/all-sections/7.json?apikey= that's without my personal apikey, which can easily obtained from https://developer.nytimes.com/signup It's `"{ results" [ { "media" : , ... }, ...] }` – meaning-matters Feb 24 '18 at 20:57

1 Answers1

3

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: [])
rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • Thanks for this extensive answer! But I'll stick with my manual traversing the object tree as it's fairly simple and easy to follow. – meaning-matters Feb 24 '18 at 20:59
  • Further thoughts: Because this is an ugly/stupid format mistake, I'm leaning towards fixing the JSON input string (little code replacing `"list":""` with `"list":[]`). I'm aware it's less generic and assumes `"list":""` does not occur elsewhere. This enables the use `Decodable`. A small pragmatic fix for an ugly problem; I like that. – meaning-matters Mar 17 '18 at 09:41