1

Hey I have this Json for Example:

{"v":{"value1":1548303671,"value2":"invalid","value3":"invalid"}}

And the model class:

struct VersionApi: Decodable {
    let data: NestedData?
    let v: [String:Int?]

    enum CodingKeys: String, CodingKey {
        case data = "data"
        case v = "v"
    }
}

When trying to decode I get this error message:

debugDescription: "Expected to decode Int but found a string/data instead.", underlyingError: nil)

I know what it means but I don't know a solution. I need a dictionary with Ints in this format: [String:Int?]. I tried to write a custom initializer like that:

init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        data = try values.decode(NestedData, forKey: .data)
        let vID: [String:Any] = try values.decode([String:Any].self, forKey: .v)
        v = try values.decode([String:Int?].self, forKey: .v)

    }

I then wanted to go through the dictionary and if Any is not Int I wanted it to set to nil. But this does not work as no candidates produce the expected type:

No 'decode' candidates produce the expected contextual result type '[String : Any]'

How can I set Int to nil if the value is not an Int?

Dávid Pásztor
  • 51,403
  • 9
  • 85
  • 116
PaFi
  • 888
  • 1
  • 9
  • 24
  • You have to implement codable @PaFi – Abhishek Jadhav Jan 24 '19 at 09:31
  • Make it String from Int @PaFi – Abhishek Jadhav Jan 24 '19 at 09:37
  • Right now its A String but a Solution where anything can be there for example also nil would be really nice. Thanks for helping! – PaFi Jan 24 '19 at 09:37
  • @abhishek what do you mean? The result should be Int? – PaFi Jan 24 '19 at 09:40
  • Make it String it works, It looks like an Int when you try to decode it found its string that's why and try to implement Codable protocol because it's wrong what you implemented in struct. – Abhishek Jadhav Jan 24 '19 at 09:44
  • If you can change that JSON, you should change the backend to send actual `null` values instead of the `"invalid"` , which would make your decoding much easier. If you cannot change it, you most probably will have to use `JSONSerialization`, `Decodable` is much better suited for correctly designed JSONs whose collections can be represented as homogenous collections in Swift. – Dávid Pásztor Jan 24 '19 at 10:15
  • Related: https://stackoverflow.com/questions/52631305/how-to-parse-json-with-decodable-protocol-when-property-types-might-change-from – user28434'mstep Jan 24 '19 at 10:22
  • @PaFi I updated the answer decode the JSON response successfully. But yeah, it will be hectic to compare `enum` cases wherever used. – Kamran Jan 24 '19 at 13:08
  • `Codable` relies on concrete and reasonably predictable types. Rather than trying to decode *I-don't-know-what-it-is* look for a service which sends consistent data. And if you were responsible for the service change that. Otherwise use traditional `JSONSerialization` and check the types. – vadian Jan 24 '19 at 14:08

1 Answers1

0

As Any is not decodable so compiler is complaining. You can first create an enum (from this answer) as below to decode/encode dynamic type,

enum BiType<L: Codable, R: Codable>: Codable {
    case left(L)
    case right(R)

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()

        if let left = try? container.decode(L.self) {
            self = .left(left)
        } else if let right = try? container.decode(R.self) {
            self = .right(right)
        } else {
            throw DecodingError
                .typeMismatch(
                    BiType<L, R>.self,
                    .init(codingPath: decoder.codingPath,
                          debugDescription: "Expected either `\(L.self)` or `\(R.self)`"))
        }
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case let .left(left):
            try container.encode(left)
        case let .right(right):
            try container.encode(right)
        }
    }
}

Now you can update VersionApi to use that type,

struct VersionApi: Decodable {
    let data: NestedData?
    let v: [String: BiType<String, Int>]

    enum CodingKeys: String, CodingKey {
        case data = "data"
        case v = "v"
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        data = try values.decode(NestedData.self, forKey: .data)
        v = try values.decode([String: BiType<String, Int>].self, forKey: .v)
    }
}

Example

let data = """
{"v":{"value1":1548303671,"value2":"invalid","value3":"invalid"}}
""".data(using: .utf8)!

do {
    let v = try JSONDecoder().decode(VersionApi.self, from: data)
    v.v.values.forEach({ (type) in
        switch type {
        case .left(let left):
            debugPrint(left)
        case .right(let right):
            debugPrint(right)
        }
    })
} catch {
    debugPrint(error)
}

Output

1548303671 
"invalid" 
"invalid"
Kamran
  • 14,987
  • 4
  • 33
  • 51
  • Is there no other way because with this solution I always have to downcast for any further usage..? but thanks! – PaFi Jan 24 '19 at 09:54
  • @PaFi, [use some `enum` instead of `Any`](https://stackoverflow.com/a/52632026/6920962) – user28434'mstep Jan 24 '19 at 10:00
  • This won't work because the `Dictionary` has heterogenous values, so you cannot decode it either as `[String:String?]` or as `[String:Int?]`. And if you could, then you shouldn't declare `v` as `[String:Any?]`, but rather have two Optional properties, one of type `[String:String?]`, the other of type `[String:Int?]` and always only assign a non-nil value to one of them. However, this is not such a simple case due to the fact that the heterogenous values are nested in a `Dictionary` so cannot be directly decoded using `JSONDecoder`. – Dávid Pásztor Jan 24 '19 at 10:07