使用 Codable 将接收到的 Int 转换为 Bool 解码 JSON

Posted

技术标签:

【中文标题】使用 Codable 将接收到的 Int 转换为 Bool 解码 JSON【英文标题】:Convert received Int to Bool decoding JSON using Codable 【发布时间】:2017-12-18 19:53:50 【问题描述】:

我有这样的结构:

struct JSONModelSettings 
    let patientID : String
    let therapistID : String
    var isEnabled : Bool

    enum CodingKeys: String, CodingKey 
        case settings // The top level "settings" key
    

    // The keys inside of the "settings" object
    enum SettingsKeys: String, CodingKey 
        case patientID = "patient_id"
        case therapistID = "therapist_id"
        case isEnabled = "is_therapy_forced"
    


extension JSONModelSettings: Decodable 
    init(from decoder: Decoder) throws 

        // Extract the top-level values ("settings")
        let values = try decoder.container(keyedBy: CodingKeys.self)

        // Extract the settings object as a nested container
        let user = try values.nestedContainer(keyedBy: SettingsKeys.self, forKey: .settings)

        // Extract each property from the nested container
        patientID = try user.decode(String.self, forKey: .patientID)
        therapistID = try user.decode(String.self, forKey: .therapistID)
        isEnabled = try user.decode(Bool.self, forKey: .isEnabled)
    

和这种格式的 JSON(用于从设置中提取键的结构,无需额外的包装器):


  "settings": 
    "patient_id": "80864898",
    "therapist_id": "78920",
    "enabled": "1"
  

问题是如何将“isEnabled”转换为 Bool,(从 API 获取 1 或 0) 当我试图解析响应时,我得到了错误: “应解码 Bool,但找到了一个数字。”

【问题讨论】:

为什么不将forKey: .isEnabled 包装在一个函数中,该函数将返回一个布尔值true 为1,false 为0? Swift 4.1 修复了这个问题 这个问题的更优雅的解决方案可以在***.com/a/51246308/621571找到 【参考方案1】:

在这些情况下,我通常喜欢将模型保留为 JSON 数据,因此在您的情况下为 Ints。比我向模型添加计算属性以转换为布尔值等

struct Model 
   let enabled: Int
 
   var isEnabled: Bool 
       return enabled == 1
   

【讨论】:

OP 一开始无法解析数据。 因为 OP 试图直接转换为 API 不返回的 bool。【参考方案2】:

我的建议是:不要与 JSON 作斗争。尽可能快地把它变成一个 Swift 值,然后在那儿进行你的操作。

您可以定义一个私有的内部结构来保存解码后的数据,如下所示:

struct JSONModelSettings 
    let patientID : String
    let therapistID : String
    var isEnabled : Bool


extension JSONModelSettings: Decodable 
    // This struct stays very close to the JSON model, to the point
    // of using snake_case for its properties. Since it's private,
    // outside code cannot access it (and no need to either)
    private struct JSONSettings: Decodable 
        var patient_id: String
        var therapist_id: String
        var enabled: String
    

    private enum CodingKeys: String, CodingKey 
        case settings
    

    init(from decoder: Decoder) throws 
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let settings  = try container.decode(JSONSettings.self, forKey: .settings)
        patientID     = settings.patient_id
        therapistID   = settings.therapist_id
        isEnabled     = settings.enabled == "1" ? true : false
    


其他 JSON 映射框架,例如 ObjectMapper 允许您将 转换函数 附加到编码/解码过程。看起来Codable 目前没有等价物。

【讨论】:

“不要与 JSON 对抗。”【参考方案3】:

属性包装器

要将Strings、Ints、Doubles 或Bools 解码为Bool

只需将@SomeKindOfBool 放在布尔属性之前,例如:

@SomeKindOfBool public var someKey: Bool

演示:

struct MyType: Decodable 
    @SomeKindOfBool public var someKey: Bool


let jsonData = """
[
  "someKey": "true" ,
  "someKey": "yes" ,
  "someKey": "1" ,

  "someKey": 1 ,

  "someKey": "false" ,
  "someKey": "no" ,
  "someKey": "0" ,

  "someKey": 0 
]
""".data(using: .utf8)!

let decodedJSON = try! JSONDecoder().decode([MyType].self, from: jsonData)

for decodedType in decodedJSON 
    print(decodedType.someKey)


这背后强大的PropertyWrapper实现:

@propertyWrapper
struct SomeKindOfBool: Decodable 
    var wrappedValue: Bool

    init(from decoder: Decoder) throws 
        let container = try decoder.singleValueContainer()

        //Handle String value
        if let stringValue = try? container.decode(String.self) 
            switch stringValue.lowercased() 
            case "false", "no", "0": wrappedValue = false
            case "true", "yes", "1": wrappedValue = true
            default: throw DecodingError.dataCorruptedError(in: container, debugDescription: "Expect true/false, yes/no or 0/1 but`\(stringValue)` instead")
            
        

        //Handle Int value
        else if let intValue = try? container.decode(Int.self) 
            switch intValue 
            case 0: wrappedValue = false
            case 1: wrappedValue = true
            default: throw DecodingError.dataCorruptedError(in: container, debugDescription: "Expect `0` or `1` but found `\(intValue)` instead")
            
        

        //Handle Int value
        else if let doubleValue = try? container.decode(Double.self) 
            switch doubleValue 
            case 0: wrappedValue = false
            case 1: wrappedValue = true
            default: throw DecodingError.dataCorruptedError(in: container, debugDescription: "Expect `0` or `1` but found `\(doubleValue)` instead")
            
        

        else 
            wrappedValue = try container.decode(Bool.self)
        
    

如果您需要实现一个可选的,请查看 this answer here

【讨论】:

【参考方案4】:

解码为String,然后将其转换为Bool,只需修改代码的一些行:

"0"是JSON字符串,不能解码为Int。)

struct JSONModelSettings 
    let patientID : String
    let therapistID : String
    var isEnabled : Bool

    enum CodingKeys: String, CodingKey 
        case settings // The top level "settings" key
    

    // The keys inside of the "settings" object
    enum SettingsKeys: String, CodingKey 
        case patientID = "patient_id"
        case therapistID = "therapist_id"
        case isEnabled = "enabled"//### "is_therapy_forced"?
    


extension JSONModelSettings: Decodable 
    init(from decoder: Decoder) throws 

        // Extract the top-level values ("settings")
        let values = try decoder.container(keyedBy: CodingKeys.self)

        // Extract the settings object as a nested container
        let user = try values.nestedContainer(keyedBy: SettingsKeys.self, forKey: .settings)

        // Extract each property from the nested container
        patientID = try user.decode(String.self, forKey: .patientID)
        therapistID = try user.decode(String.self, forKey: .therapistID)

        //### decode the value for "enabled" as String
        let enabledString = try user.decode(String.self, forKey: .isEnabled)
        //### You can throw type mismatching error when `enabledString` is neither "0" or "1"
        if enabledString != "0" && enabledString != "1" 
            throw DecodingError.typeMismatch(Bool.self, DecodingError.Context(codingPath: user.codingPath + [SettingsKeys.isEnabled], debugDescription: "value for \"enabled\" needs to be \"0\" or \"1\""))
        
        //### and convert it to Bool
        isEnabled = enabledString != "0"
    

【讨论】:

【参考方案5】:

现在是 2021 年,我们有更简单的方法在 Swift 5 中使用 PropertyWrappers 解决这个问题。

@propertyWrapper
struct BoolFromInt: Decodable 
    var wrappedValue: Bool // or use `let` to make it immutable
    
    init(from decoder: Decoder) throws 
        let container = try decoder.singleValueContainer()
        let intValue = try container.decode(Int.self)
        switch intValue 
        case 0: wrappedValue = false
        case 1: wrappedValue = true
        default: throw DecodingError.dataCorruptedError(in: container, debugDescription: "Expected `0` or `1` but received `\(intValue)`")
        
    

用法:

struct Settings: Decodable 
    @BoolFromInt var isEnabled: Bool

【讨论】:

以上是关于使用 Codable 将接收到的 Int 转换为 Bool 解码 JSON的主要内容,如果未能解决你的问题,请参考以下文章

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

如何使用解码器将给定 JSON 中 Double 类型的 Codable 属性转换为 Date?

使用 Swift 将 UInt8?、String、Int 转换为 String 类型

java如何将接收到的数字自动转换为枚举

如何转换从管道接收到的二进制数

Swift 4 Codable - API 有时提供 Int 有时提供 String