使用 Swift 4 的 Codable 解码时,是什么阻止了我从字符串到 Int 的转换?
What Is Preventing My Conversion From String to Int When Decoding Using Swift 4’s 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
协议做错了什么?如果不是,那么是什么导致我的第二次测试因这个看似简单的转换而失败?
您看到的问题与 init(with: Decoder)
的编写方式以及 DataDecoder
类型如何解码其类型参数有关。由于测试失败 nil != Optional(5)
,因此 uvIndex
或 currentWeatherReport
是 currentWeatherReport?.uvIndex
.
中的 nil
让我们看看 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?
}
嗯。因此,如果解码为 Int
和 String
都失败(因为值不存在),您将得到 nil
。但显然,情况并非总是如此,因为第一个测试通过了(并且确实存在一个值)。
那么,进入下一个故障模式:如果 Int
显然正在解码,为什么 String
没有正确解码?那么,当 uvIndex
是 String
时,仍然进行以下解码调用:
try container.decodeIfPresent(Int.self, forKey: .uvIndex)
如果值不存在(即给定键没有值,或者值明确为 null
),则仅调用 returns nil
;如果值 存在 但不是 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?
s 起,错误被吞没,返回 nil
。 nil
进入了最初的 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
}
}
你可以试试SafeDecoder
import SafeDecoder
internal struct CurrentWeatherReport: Codable {
internal var uvIndex: Int?
}
然后照常解码即可。
我从网络请求中得到以下 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
协议做错了什么?如果不是,那么是什么导致我的第二次测试因这个看似简单的转换而失败?
您看到的问题与 init(with: Decoder)
的编写方式以及 DataDecoder
类型如何解码其类型参数有关。由于测试失败 nil != Optional(5)
,因此 uvIndex
或 currentWeatherReport
是 currentWeatherReport?.uvIndex
.
nil
让我们看看 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?
}
嗯。因此,如果解码为 Int
和 String
都失败(因为值不存在),您将得到 nil
。但显然,情况并非总是如此,因为第一个测试通过了(并且确实存在一个值)。
那么,进入下一个故障模式:如果 Int
显然正在解码,为什么 String
没有正确解码?那么,当 uvIndex
是 String
时,仍然进行以下解码调用:
try container.decodeIfPresent(Int.self, forKey: .uvIndex)
如果值不存在(即给定键没有值,或者值明确为 null
),则仅调用 returns nil
;如果值 存在 但不是 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?
s 起,错误被吞没,返回 nil
。 nil
进入了最初的 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
}
}
你可以试试SafeDecoder
import SafeDecoder
internal struct CurrentWeatherReport: Codable {
internal var uvIndex: Int?
}
然后照常解码即可。