JSONDecoder().decode([String: Any].self, from: data) no go!

May be a repeat - search didn't turn it up but people on StackOverflow said it had been posted.


I'm trying to use the new JSONDecoder to unravel some JSON objects. Yikes! I find I cannot use JSONDecoder for complex dictionaries:


let dict = try? JSONDecoder().decode([String: Any].self, from: data)


doesn't work because Any isn't decodeable. OK, lets make a new type that is:


typealias Foo = Any & Decodable
let dict = try? JSONDecoder().decode([String: Foo].self, from: data)


Nope that doesn't work either. The "solutions" on StackOverflow all involve writing a LOT of code to get around this.


Really? I cannot believe that what use to work is now impossible! Tell me it isn't so!


Or, is the solution to throw in some ObjectiveC code into my project to do this decoding?

Accepted Reply

It's not that no one sees it as a problem (one way another, this issue keeps coming up on forums.swift.org), but that the Swift team is still some way away from providing a solution.


So, for informational purposes, let's tease this apart:


The situation has changed a bit since the thread you linked to. At that time, arrays and dictionaries declared conformance with Codable, but failed at run time if their contents weren't Codable. In the current version of Swift, it's possible to have conditional conformance, since the idea of "this collection is Codable if the types of its elements are Codable" can now be expressed at compile time.


Your Foo typealias isn't really necessary, because "Any & Decodable" is basically the same thing as just "Decodable". It doesn't work in decode([String: Decodable].self, from: data) because in Swift, protocols don't conform to themselves, for technical reasons. For now, what you would need instead is a "type-erased type" that conforms to Decodable. Swift defines a number of these type-erased type (such as AnyHashable, conforming to the protocol Hashable), but there isn't one for Decodable.


What you do about this depends on the nature of your JSON data. If you know the incoming dictionary keys, and you know the JSON types of all the values at all levels, the best solution is to declare a hierarchy of custom types that individually conform to Codable, relying on synthesis of the conforming implementations so you don't have to write them yourself.


If you don't know the incoming dictionary keys in advance, or if the overall hierarchical structure isn't known in advance, then a better solution is to skip JSONDecoder, and use JSONSerialization directly. You can then test and bridge the resulting hierarchical data. This tends to involve a fair amount of grunt work, so you can look for one of the Swift JSON decoder libraries on (say) GitHub for something that does much of the work for you. (Typically, such libraries will return values of an enum with cases like .dictionary, .array, .integer, .double, .date, etc, with the actual values as associated values of the enum cases.)


If you want to work a bit harder, you can make your own type-erased AnyDecodable type. Depending on the nature of the data, this may or may not reduce the amount of glue code you would have to write for the JSONSerialization solution. I suggest you start with something like this: mikeash.com/pyblog/friday-qa-2017-12-08-type-erasure-in-swift.html

Replies

Well I found the original thread - but in the end there is no solution - it just won't work (Swift 4).


:-(


I can't belive no one on the Swift team sees this as a serious problem...


https://forums.developer.apple.com/message/265436#265436

It's not that no one sees it as a problem (one way another, this issue keeps coming up on forums.swift.org), but that the Swift team is still some way away from providing a solution.


So, for informational purposes, let's tease this apart:


The situation has changed a bit since the thread you linked to. At that time, arrays and dictionaries declared conformance with Codable, but failed at run time if their contents weren't Codable. In the current version of Swift, it's possible to have conditional conformance, since the idea of "this collection is Codable if the types of its elements are Codable" can now be expressed at compile time.


Your Foo typealias isn't really necessary, because "Any & Decodable" is basically the same thing as just "Decodable". It doesn't work in decode([String: Decodable].self, from: data) because in Swift, protocols don't conform to themselves, for technical reasons. For now, what you would need instead is a "type-erased type" that conforms to Decodable. Swift defines a number of these type-erased type (such as AnyHashable, conforming to the protocol Hashable), but there isn't one for Decodable.


What you do about this depends on the nature of your JSON data. If you know the incoming dictionary keys, and you know the JSON types of all the values at all levels, the best solution is to declare a hierarchy of custom types that individually conform to Codable, relying on synthesis of the conforming implementations so you don't have to write them yourself.


If you don't know the incoming dictionary keys in advance, or if the overall hierarchical structure isn't known in advance, then a better solution is to skip JSONDecoder, and use JSONSerialization directly. You can then test and bridge the resulting hierarchical data. This tends to involve a fair amount of grunt work, so you can look for one of the Swift JSON decoder libraries on (say) GitHub for something that does much of the work for you. (Typically, such libraries will return values of an enum with cases like .dictionary, .array, .integer, .double, .date, etc, with the actual values as associated values of the enum cases.)


If you want to work a bit harder, you can make your own type-erased AnyDecodable type. Depending on the nature of the data, this may or may not reduce the amount of glue code you would have to write for the JSONSerialization solution. I suggest you start with something like this: mikeash.com/pyblog/friday-qa-2017-12-08-type-erasure-in-swift.html

Quincey prompted me to think more on this issue. In the end the solution is way easier than I would have expected. One of my JSON objects:


let JSON: [String: Any] = [
  "links": [
    "self": "https:/
  ],
  "data": [
    "type": "gateway",
    "id": "5aa99d68cf4c5b0619e74d0d",
    "attributes": [
      "is_sandbox": false,
      "provider": "hmmmmm",
      "configuration": [
        "pin": "1234",
        "software_id": "A48C",
        "source_key": "21496568wE"
      ]
    ]
  ],
  "jsonapi": [
    "version": "1.0"
  ]
]


So now, I need to model it so that every component is 'Decodable' - and guess what, [String: String] is. As you can see, I nested types, which allows me to use dot syntax to navigate but reduce name polutiuon:


struct Foo: Decodable {
    struct Dddd: Decodable {
        struct Aaaa: Decodable {
            let is_sandbox: Bool
            let provider: String
            let configuration: [String: String]
        }
        let type: String
        let id: String
        let attributes: Aaaa
    }
    let links: [String: String]
    let data: Dddd
    let jsonapi: [String: String]
}


Finally, lets test it out. Note that 'jsonData' is what I receive from the web service:


if let jsonData = try? JSONSerialization.data(withJSONObject: JSON, options: [])
{
    do {
        let foo = try JSONDecoder().decode(Foo.self, from: jsonData)
        print("JSON", foo.data.attributes.provider)
    } catch {
        print("ERROR:", error)
    }
}


Again, Quincey - thanks so much for prompting me to work this out on my own.