使用 Swift 4 的 Codable 进行解码时,是啥阻止了我从 String 到 Int 的转换?

Posted

技术标签:

【中文标题】使用 Swift 4 的 Codable 进行解码时,是啥阻止了我从 String 到 Int 的转换?【英文标题】:What Is Preventing My Conversion From String to Int When Decoding Using Swift 4’s Codable?使用 Swift 4 的 Codable 进行解码时,是什么阻止了我从 String 到 Int 的转换? 【发布时间】:2018-01-30 04:03:34 【问题描述】:

我从网络请求中得到以下 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 协议,我是否做错了什么?如果不是,那么是什么导致我的第二次测试在这个看似简单的演员阵容中失败?

【问题讨论】:

【参考方案1】:

您看到的问题与您的init(with: Decoder) 的编写方式以及DataDecoder 类型如何解码其类型参数有关。由于测试以nil != Optional(5) 失败,那么uvIndexcurrentWeatherReportcurrentWeatherReport?.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?

嗯。因此,如果解码为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?s,错误被吞并,返回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
    

【讨论】:

完美。您的实现按预期工作,并且您的解释很好理解。 @NickKohrn 太好了,很高兴能帮上忙!【参考方案2】:

你可以试试SafeDecoder

import SafeDecoder

internal struct CurrentWeatherReport: Codable 

  internal var uvIndex: Int?


然后像往常一样解码。

【讨论】:

以上是关于使用 Swift 4 的 Codable 进行解码时,是啥阻止了我从 String 到 Int 的转换?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 swift 4 Codable 中手动解码数组?

Swift 4 可编码;如何使用单个根级密钥解码对象

如何在 Core Data 中使用 swift 4 Codable?

如何在 Core Data 中使用 swift 4 Codable?

如何在 Core Data 中使用 swift 4 Codable?

使用 swift Codable 以值作为键来解码 JSON