使用Swift 4的Codable进行解码时,什么阻止我从字符串转换为Int?

时间:2018-01-30 04:03:34

标签: swift codable

我从网络请求中收到以下JSON:

{
    "uvIndex": 5
}

我使用以下类型解码字符串到数据的结果:

internal final class DataDecoder<T: Decodable> {

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

}

以下是要将数据转换为的模型:

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

}

以下是用于验证解码的测试:

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

}

测试之间的差异是JSON中uvIndex的值;一个是字符串,另一个是整数。我期待一个整数,但我想处理任何情况,其中值可以作为字符串而不是数字返回,因为这似乎是我使用的一些API的常见做法。但是,我的第二个测试仍然失败,显示以下消息:XCTAssertEqual failed: ("nil") is not equal to ("Optional(5)") -

我是否因为导致此失败的Codable协议而做错了什么?如果没有,那么是什么导致我的第二次测试失败了这个看似简单的演员呢?

Screenshot of failing unit test in Xcode

2 个答案:

答案 0 :(得分:1)

您所看到的问题与init(with: Decoder)的编写方式以及DataDecoder类型如何解码其类型参数有关。由于测试失败了nil != Optional(5),因此uvIndex中的currentWeatherReportnilcurrentWeatherReport?.uvIndex

让我们看看uvIndex可能是nil的方式。由于它是Int?,如果没有初始化,它会获得默认值nil,因此这是一个开始寻找的好地方。如何分配其默认值?

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

嗯。因此,如果解码为IntString都失败(因为该值不存在),您将获得nil。但很明显,情况并非总是如此,因为第一次测试通过(并且确实存在一个值)。

那么,到下一个失败模式:如果Int明显被解码,为什么String没有正确解码?好吧,当uvIndexString时,仍会进行以下解码调用:

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

如果没有值,则该调用仅返回nil(即,给定键没有值,或者值明确为null);如果 值存在但不是Int,则呼叫将throw

由于未捕获并显式处理抛出的错误,因此它会立即传播,从不调用try container.decodeIfPresent(String.self, forKey: .uvIndex)。相反,错误会在CurrentWeatherReport被解码的地方冒泡:

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

从此代码try?开始,错误被误吞,返回nilnil调用最终为currentWeatherReport?.uvIndex调用,最终为nil而不是因为uvIndex丢失,而是因为整个报告无法解码。

可能,符合您需求的init(with: Decoder)实现更符合以下几点:

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

答案 1 :(得分:0)

您可以尝试SafeDecoder

import SafeDecoder

internal struct CurrentWeatherReport: Codable {

  internal var uvIndex: Int?

}

然后照常解码。