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

家住魔仙堡 提交于 2019-11-28 08:57:35

问题


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

{
    "uvIndex": 5
}

I use the following type to decode the string-to-data result:

internal final class DataDecoder<T: Decodable> {

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

}

The following is the model that the data is to be transformed into:

internal struct CurrentWeatherReport: Codable {

    // MARK: Properties

    internal var uvIndex: Int?

    // MARK: CodingKeys

    private enum CodingKeys: String, CodingKey {
        case uvIndex
    }

    // MARK: Initializers

    internal init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if let uvIndexInteger = try container.decodeIfPresent(Int.self, forKey: .uvIndex) {
            uvIndex = uvIndexInteger
        } else if let uvIndexString = try container.decodeIfPresent(String.self, forKey: .uvIndex) {
            uvIndex = Int(uvIndexString)
        }
    }

}

The following are the tests used to verify the decoding:

internal final class CurrentWeatherReportTests: XCTestCase {

    internal final func test_CurrentWeather_ReturnsExpected_UVIndex_FromIntegerValue() {
        let string =
        """
        {
            "uvIndex": 5
        }
        """
        let data = DataUtility.data(from: string)
        let currentWeatherReport = DataDecoder<CurrentWeatherReport>.decode(data)
        XCTAssertEqual(currentWeatherReport?.uvIndex, 5)
    }

    internal final func test_CurrentWeather_ReturnsExpected_UVIndex_FromStringValue() {
        let string =
        """
        {
            "uvIndex": "5"
        }
        """
        let data = DataUtility.data(from: string)
        let currentWeatherReport = DataDecoder<CurrentWeatherReport>.decode(data)
        XCTAssertEqual(currentWeatherReport?.uvIndex, 5)
    }

}

The difference between the tests is the value of uvIndex in the JSON; one is a string and the other is an integer. I am expecting an integer, but I want to handle any cases in which the value may come back as a string rather than a number since that seems to be a common practice with a few APIs that I work with. However, my second test keeps failing with the following message: XCTAssertEqual failed: ("nil") is not equal to ("Optional(5)") -

Am I doing something incorrectly with regard to the Codable protocol that is causing this failure? If not, then what is causing my second test to fail with this seemingly-simple cast?


回答1:


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
    }
}


来源:https://stackoverflow.com/questions/48513704/what-is-preventing-my-conversion-from-string-to-int-when-decoding-using-swift-4

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!