【问题标题】:Swift structures: handling multiple types for a single propertySwift 结构:为单个属性处理多种类型
【发布时间】:2017-10-15 19:21:06
【问题描述】:

我正在使用 Swift 4 并尝试解析一些 JSON 数据,这些数据显然在某些情况下对于同一个键可能具有不同的类型值,例如:

{
    "type": 0.0
}

{
    "type": "12.44591406"
}

我实际上坚持定义我的struct,因为我不知道如何处理这种情况,因为

struct ItemRaw: Codable {
    let parentType: String

    enum CodingKeys: String, CodingKey {
        case parentType = "type"
    }
}

抛出"Expected to decode String but found a number instead.",自然而然,

struct ItemRaw: Codable {
    let parentType: Float

    enum CodingKeys: String, CodingKey {
        case parentType = "type"
    }
}

相应地抛出"Expected to decode Float but found a string/data instead."

在定义我的struct 时如何处理这种(和类似的)情况?

【问题讨论】:

    标签: json swift data-structures swift4 swift-structs


    【解决方案1】:

    我在尝试解码/编码 Reddit 列表 JSON 响应中的“已编辑”字段时遇到了同样的问题。我创建了一个结构,它表示给定键可能存在的动态类型。键可以是布尔值或整数。

    { "edited": false }
    { "edited": 123456 }
    

    如果你只需要能够解码,只需实现 init(from:)。如果您需要双向使用,则需要实现 encode(to:) 函数。

    struct Edited: Codable {
        let isEdited: Bool
        let editedTime: Int
    
        // Where we determine what type the value is
        init(from decoder: Decoder) throws {
            let container =  try decoder.singleValueContainer()
    
            // Check for a boolean
            do {
                isEdited = try container.decode(Bool.self)
                editedTime = 0
            } catch {
                // Check for an integer
                editedTime = try container.decode(Int.self)
                isEdited = true
            }
        }
    
        // We need to go back to a dynamic type, so based on the data we have stored, encode to the proper type
        func encode(to encoder: Encoder) throws {
            var container = encoder.singleValueContainer()
            try isEdited ? container.encode(editedTime) : container.encode(false)
        }
    }
    

    在我的 Codable 类中,然后我使用我的结构。

    struct Listing: Codable {
        let edited: Edited
    }
    

    编辑:针对您的场景的更具体的解决方案

    我建议在解码时使用 CodingKey 协议和枚举来存储所有属性。当您创建符合 Codable 的内容时,编译器将为您创建一个私有枚举 CodingKeys。这使您可以根据 JSON 对象属性键来决定要执行的操作。

    例如,这是我正在解码的 JSON:

    {"type": "1.234"}
    {"type": 1.234}
    

    如果您想从 String 转换为 Double 因为您只需要 double 值,只需解码字符串,然后从中创建一个 double。 (这就是 Itai Ferber 正在做的事情,然后您还必须使用 try decoder.decode(type:forKey:) 解码所有属性)

    struct JSONObjectCasted: Codable {
        let type: Double?
    
        init(from decoder: Decoder) throws {
            // Decode all fields and store them
            let container = try decoder.container(keyedBy: CodingKeys.self) // The compiler creates coding keys for each property, so as long as the keys are the same as the property names, we don't need to define our own enum.
    
            // First check for a Double
            do {
                type = try container.decode(Double.self, forKey: .type)
    
            } catch {
                // The check for a String and then cast it, this will throw if decoding fails
                if let typeValue = Double(try container.decode(String.self, forKey: .type)) {
                    type = typeValue
                } else {
                    // You may want to throw here if you don't want to default the value(in the case that it you can't have an optional).
                    type = nil
                }
            }
    
            // Perform other decoding for other properties.
        }
    }
    

    如果您需要将类型与值一起存储,您可以使用符合 Codable 的枚举而不是结构。然后,您可以使用带有 JSONObjectCustomEnum 的“type”属性的 switch 语句并根据情况执行操作。

    struct JSONObjectCustomEnum: Codable {
        let type: DynamicJSONProperty
    }
    
    // Where I can represent all the types that the JSON property can be. 
    enum DynamicJSONProperty: Codable {
        case double(Double)
        case string(String)
    
        init(from decoder: Decoder) throws {
            let container =  try decoder.singleValueContainer()
    
            // Decode the double
            do {
                let doubleVal = try container.decode(Double.self)
                self = .double(doubleVal)
            } catch DecodingError.typeMismatch {
                // Decode the string
                let stringVal = try container.decode(String.self)
                self = .string(stringVal)
            }
        }
    
        func encode(to encoder: Encoder) throws {
            var container = encoder.singleValueContainer()
            switch self {
            case .double(let value):
                try container.encode(value)
            case .string(let value):
                try container.encode(value)
            }
        }
    }
    

    【讨论】:

    • try container.encode(isEdited ? editedTime : false)
    • @LeoDabus,如果该表达式有效,那就太好了。 Swift 4 编译器在使用三元运算符时不允许类型不匹配。
    • 这应该可以try isEdited ? container.encode(editedTime) : container.encode(false)
    • 您确实是正确的,感谢您的提醒。我会根据您的建议更新 sn-p。
    • 我认为你应该在解码editedTime时也发现任何错误。它看起来不太好,但我认为正确的语法是do { isEdited = try container.decode(Bool.self) editedTime = 0 } catch { do { editedTime = try container.decode(Int.self) isEdited = true } catch { editedTime = 0 isEdited = false } }
    【解决方案2】:

    一个简单的解决方案是提供init(from:) 的实现,它尝试将值解码为String,如果由于类型错误而失败,则尝试解码为Double

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        do {
            self.parentType = try container.decode(String.self, forKey: .parentType)
        } catch DecodingError.typeMismatch {
            let value = try container.decode(Double.self, forKey: .parentType)
            self.parentType = "\(value)"
        }
    }
    

    【讨论】:

    • 请注意,这在实现 class 时有效,但不适用于 structs,我认为您应该删除 required 才能有效结构...
    • @vadian 我正在我的操场上尝试这段代码,我认为不需要public func...
    • @AhmadF 我刚刚取消了(更糟糕的)编辑,不多不少。
    • 虽然这个答案是正确的(有一个小例外),但只有当我的 struct 包含一个属性时它才会起作用。当我开始添加更多属性时,我遇到了错误Return from initializer without initializing all stored properties。我现在明白错误的含义,也许我可以在我的问题中声明我将拥有其他属性......不过我会支持你,但我会接受另一个答案,因为它被放在整个上下文中并奖励有一定声誉的新用户 :) 非常感谢您的帮助!
    • @errata 呃,好的。如果您有其他属性,是的,您还需要解码和初始化它们,就像在 Swift 中的任何其他初始化程序中一样。
    【解决方案3】:

    我必须解码作为字符串给出的PHP/MySQL/PDO double 值,对于这个用例我必须扩展KeyedDecodingContainer,如下所示:

    extension KeyedDecodingContainer {
        func decode(forKey key: KeyedDecodingContainer.Key) throws -> Double {
            do {
                let str = try self.decode(String.self, forKey: key)
                if let dbl = Double(str) {
                    return dbl
                }
            } catch DecodingError.typeMismatch {
                return try self.decode(Double.self, forKey: key)
            }
            let context = DecodingError.Context(codingPath: self.codingPath,
                                                debugDescription: "Wrong Money Value")
            throw DecodingError.typeMismatch(Double.self, context)
        }
    }
    

    用法:

    let data = """
    {"value":"1.2"}
    """.data(using: .utf8)!
    
    struct Test: Decodable {
        let value: Double
        enum CodingKeys: String, CodingKey {
            case value
        }
        init(from decoder: Decoder) throws {
            self.value = try decoder.container(keyedBy: CodingKeys.self)
                                    .decode(forKey: CodingKeys.value)
        }
    }
    try JSONDecoder().decode(Test.self, from: data).value
    

    【讨论】:

    • 任何建议如何解析 Int 而不是字符串?谢谢
    【解决方案4】:

    // 输出 Json

    {
       "software_id": "10",
        "name": "Kroll"
    },
    {
       "software_id": 580,
        "name": "Synmed"
    }
    

    // 可编码结构

    struct SoftwareDataModel: Codable {
    
     var softwareId:MyValue?
     var name:String?
    
     enum CodingKeys: String, CodingKey{
        case softwareId = "software_id"
        case name
     }
    }
    

    MYValue 是 Codable Struct,它有助于将您的数据类型转换为“字符串”,这里我只提到字符串和 Int 数据类型。

    enum MyValue: Codable {
    
     case string(String)
    
     var stringValue: String? {
        switch self {
        case .string(let s):
            return s
        }
     }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let x = try? container.decode(String.self) {
            self = .string(x)
            return
        }
        if let x = try? container.decode(Int.self) {
            self = .string("\(x)")
            return
        }
        throw DecodingError.typeMismatch(MyValue.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for MyValue"))
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case .string(let x):
            try container.encode(x)
        }
    }
    

    }

    // 如何获取software_id?

    let softwareId =  Struct_object.softwareId?.stringValue ?? "0"
    

    【讨论】:

      猜你喜欢
      • 2019-05-29
      • 1970-01-01
      • 1970-01-01
      • 2023-03-18
      • 1970-01-01
      • 1970-01-01
      • 2018-09-07
      • 2017-11-18
      相关资源
      最近更新 更多