I am getting the following JSON back from a network request:
{
\"uvIndex\": 5
}
I use the fol
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
}
}
You could try SafeDecoder
import SafeDecoder
internal struct CurrentWeatherReport: Codable {
internal var uvIndex: Int?
}
Then just decode as usual.