What Is Preventing My Conversion From String to Int When Decoding Using Swift 4’s Codable?

前端 未结 2 1996
面向向阳花
面向向阳花 2020-12-21 08:08

I am getting the following JSON back from a network request:

{
    \"uvIndex\": 5
}

I use the fol

相关标签:
2条回答
  • 2020-12-21 08:36

    The issue you're seeing has to do with a combination of how your init(with: Decoder) is written, and how DataDecoder type decodes its type argument. Since the test is failing with nil != Optional(5), then either uvIndex or currentWeatherReport is nil in currentWeatherReport?.uvIndex.

    Let's see how uvIndex might be nil. Since it's an Int?, it gets a default value of nil if not otherwise initialized, so that's a good place to start looking. How might it get assigned its default value?

    internal init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if let uvIndexInteger = try container.decodeIfPresent(Int.self, forKey: .uvIndex) {
            // Clearly assigned to here
            uvIndex = uvIndexInteger
        } else if let uvIndexString = try container.decodeIfPresent(String.self, forKey: .uvIndex) {
            // Clearly assigned to here
            uvIndex = Int(uvIndexString)
        }
    
        // Hmm, what happens if neither condition is true?
    }
    

    Hmm. So, if decoding as both an Int and a String fail (because the value isn't present), you'll get nil. But clearly, this isn't always the case, since the first test passes (and a value is indeed there).

    So, on to the next failure mode: if an Int is clearly being decoded, why is the String not decoding properly? Well, when uvIndex is a String, the following decode call is still made:

    try container.decodeIfPresent(Int.self, forKey: .uvIndex)
    

    That call only returns nil if a value isn't present (i.e. there's no value for the given key, or the value is explicitly null); if a value is present but is not an Int, the call will throw.

    Since the error being thrown isn't caught and explicitly handled, it propagates up immediately, never calling try container.decodeIfPresent(String.self, forKey: .uvIndex). Instead, the error bubbles up to where CurrentWeatherReport is decoded:

    internal final class func decode(_ data: Data) -> T? {
        return try? JSONDecoder().decode(T.self, from: data)
    }
    

    Since this code try?s, the error gets swallowed up, returning nil. That nil makes its way to the original currentWeatherReport?.uvIndex call, which ends up being nil not because uvIndex was missing, but because the whole report failed to decode.

    Likely, the init(with: Decoder) implementation that fits your needs is more along the lines of the following:

    internal init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
    
        // try? container.decode(...) returns nil if the value was the wrong type or was missing.
        // You can also opt to try container.decode(...) and catch the error.
        if let uvIndexInteger = try? container.decode(Int.self, forKey: .uvIndex) {
            uvIndex = uvIndexInteger
        } else if let uvIndexString = try? container.decode(String.self, forKey: .uvIndex) {
            uvIndex = Int(uvIndexString)
        } else {
            // Not strictly necessary, but might be clearer.
            uvIndex = nil
        }
    }
    
    0 讨论(0)
  • 2020-12-21 08:44

    You could try SafeDecoder

    import SafeDecoder
    
    internal struct CurrentWeatherReport: Codable {
    
      internal var uvIndex: Int?
    
    }
    

    Then just decode as usual.

    0 讨论(0)
提交回复
热议问题