使用 Swift 4 中的 JSONDecoder,缺少的键可以使用默认值而不是可选属性吗?

Posted

技术标签:

【中文标题】使用 Swift 4 中的 JSONDecoder,缺少的键可以使用默认值而不是可选属性吗?【英文标题】:With JSONDecoder in Swift 4, can missing keys use a default value instead of having to be optional properties? 【发布时间】:2017-06-15 19:18:15 【问题描述】:

Swift 4 添加了新的Codable 协议。当我使用 JSONDecoder 时,它似乎要求我的 Codable 类的所有非可选属性在 JSON 中具有键,否则会引发错误。

让我的类的每个属性都是可选的似乎是不必要的麻烦,因为我真正想要的是使用 json 中的值或默认值。 (我不希望该属性为零。)

有没有办法做到这一点?

class MyCodable: Codable 
    var name: String = "Default Appleseed"


func load(input: String) 
    do 
        if let data = input.data(using: .utf8) 
            let result = try JSONDecoder().decode(MyCodable.self, from: data)
            print("name: \(result.name)")
        
     catch  
        print("error: \(error)")
        // `Error message: "Key not found when expecting non-optional type
        // String for coding key \"name\""`
    


let goodInput = "\"name\": \"Jonny Appleseed\" "
let badInput = ""
load(input: goodInput) // works, `name` is Jonny Applessed
load(input: badInput) // breaks, `name` required since property is non-optional

【问题讨论】:

如果我的 json 中有多个键并且我想编写一个通用方法来映射 json 以创建对象而不是给出 nil 它应该至少给出默认值,我该怎么办。class="comcopy">跨度> 【参考方案1】:

您可以在您的类型中实现init(from decoder: Decoder) 方法,而不是使用默认实现:

class MyCodable: Codable 
    var name: String = "Default Appleseed"

    required init(from decoder: Decoder) throws 
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if let name = try container.decodeIfPresent(String.self, forKey: .name) 
            self.name = name
        
    

您也可以将name 设为常量属性(如果您愿意的话):

class MyCodable: Codable 
    let name: String

    required init(from decoder: Decoder) throws 
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if let name = try container.decodeIfPresent(String.self, forKey: .name) 
            self.name = name
         else 
            self.name = "Default Appleseed"
        
    

required init(from decoder: Decoder) throws 
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.name = try container.decodeIfPresent(String.self, forKey: .name) ?? "Default Appleseed"

您的评论:带有自定义扩展名

extension KeyedDecodingContainer 
    func decodeWrapper<T>(key: K, defaultValue: T) throws -> T
        where T : Decodable 
        return try decodeIfPresent(T.self, forKey: key) ?? defaultValue
    

您可以将 init 方法实现为

required init(from decoder: Decoder) throws 
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.name = try container.decodeWrapper(key: .name, defaultValue: "Default Appleseed")

但这并不比

短多少
    self.name = try container.decodeIfPresent(String.self, forKey: .name) ?? "Default Appleseed"

【讨论】:

另请注意,在这种特殊情况下,您可以使用自动生成的CodingKeys 枚举(因此可以删除自定义定义):) @Hamish:我第一次尝试时它没有编译,但现在它可以工作了:) 自动生成的方法无法从非可选项中读取默认值仍然很荒谬。我有 8 个可选项和 1 个非可选项,所以现在手动编写 Encoder 和 Decoder 方法会带来很多样板文件。 ObjectMapper 处理得很好。 当我们使用codable 时这真的很烦人,但仍然必须自定义 json 中缺少的键:( @LeoDabus 是不是您符合Decodable 并且还提供了您自己的init(from:) 实现?在这种情况下,编译器假定您想自己手动处理解码,因此不会为您合成 CodingKeys 枚举。正如您所说,符合Codable 反而有效,因为现在编译器正在为您合成encode(to:),因此也合成CodingKeys。如果您还提供自己的encode(to:) 实现,CodingKeys 将不再被合成。【参考方案2】:

如果未找到 JSON 键,您可以使用默认为所需值的计算属性。

class MyCodable: Codable 
    var name: String  return _name ?? "Default Appleseed" 
    var age: Int?

    // this is the property that gets actually decoded/encoded
    private var _name: String?

    enum CodingKeys: String, CodingKey 
        case _name = "name"
        case age
    

如果你想让属性读写,你也可以实现setter:

var name: String 
    get  _name ?? "Default Appleseed" 
    set  _name = newValue 

这会增加一些额外的冗长,因为您需要声明另一个属性,并且需要添加 CodingKeys 枚举(如果还没有的话)。优点是您不需要编写自定义的解码/编码代码,这在某些时候会变得乏味。

请注意,此解决方案仅在 JSON 键的值包含字符串或不存在时才有效。如果 JSON 可能具有其他形式的值(例如它的 int),那么您可以尝试this solution。

【讨论】:

有趣的方法。它确实添加了一些代码,但在创建对象后非常清晰和可检查。 我最喜欢这个问题的答案。它允许我仍然使用默认的 JSONDecoder 并轻松地为一个变量设置异常。谢谢。 注意:使用这种方法,您的属性将变为 get-only,您不能直接为该属性赋值。 @Ganpat 好点,我更新了答案以提供对读写属性的支持。谢谢,【参考方案3】:

我更喜欢的方法是使用所谓的 DTO - 数据传输对象。 它是一个结构,符合 Codable 并表示所需的对象。

struct MyClassDTO: Codable 
    let items: [String]?
    let otherVar: Int?

然后,您只需使用该 DTO 初始化您想在应用程序中使用的对象。

 class MyClass 
    let items: [String]
    var otherVar = 3
    init(_ dto: MyClassDTO) 
        items = dto.items ?? [String]()
        otherVar = dto.otherVar ?? 3
    

    var dto: MyClassDTO 
        return MyClassDTO(items: items, otherVar: otherVar)
    

这种方法也很好,因为您可以根据需要重命名和更改最终对象。 与手动解码相比,它很清晰并且需要更少的代码。 此外,通过这种方法,您可以将网络层与其他应用程序分开。

【讨论】:

其他一些方法效果很好,但最终我认为这些方法是最好的方法。 众所周知,但代码重复太多。我更喜欢 Martin R 的答案 如果您使用 app.quicktype.io 之类的服务从您的 JSON 生成 DTO,则不会出现代码重复。实际上打字会更少【参考方案4】:

你可以实现。

struct Source : Codable 

    let id : String?
    let name : String?

    enum CodingKeys: String, CodingKey 
        case id = "id"
        case name = "name"
    

    init(from decoder: Decoder) throws 
        let values = try decoder.container(keyedBy: CodingKeys.self)
        id = try values.decodeIfPresent(String.self, forKey: .id) ?? ""
        name = try values.decodeIfPresent(String.self, forKey: .name)
    

【讨论】:

是的,这是最简洁的答案,但是当你有大对象时它仍然会得到很多代码!【参考方案5】:

我在寻找完全相同的东西时遇到了这个问题。尽管我担心这里的解决方案将是唯一的选择,但我找到的答案并不是很令人满意。

就我而言,创建自定义解码器需要大量难以维护的样板文件,因此我一直在寻找其他答案。

我遇到了this article,它展示了一种在简单情况下使用@propertyWrapper 来克服这个问题的有趣方法。对我来说最重要的是它是可重用的,并且需要对现有代码进行最少的重构。

本文假设您希望缺少的布尔属性默认为 false 而不会失败,但也显示了其他不同的变体。 您可以更详细地阅读它,但我将展示我为我的用例所做的工作。

就我而言,我有一个 array,如果缺少密钥,我希望将其初始化为空。

所以,我声明了以下 @propertyWrapper 和其他扩展:

@propertyWrapper
struct DefaultEmptyArray<T:Codable> 
    var wrappedValue: [T] = []


//codable extension to encode/decode the wrapped value
extension DefaultEmptyArray: Codable 
    
    func encode(to encoder: Encoder) throws 
        try wrappedValue.encode(to: encoder)
    
    
    init(from decoder: Decoder) throws 
        let container = try decoder.singleValueContainer()
        wrappedValue = try container.decode([T].self)
    
    


extension KeyedDecodingContainer 
    func decode<T:Decodable>(_ type: DefaultEmptyArray<T>.Type,
                forKey key: Key) throws -> DefaultEmptyArray<T> 
        try decodeIfPresent(type, forKey: key) ?? .init()
    

这种方法的优点是您可以通过简单地将@propertyWrapper 添加到属性中来轻松解决现有代码中的问题。就我而言:

@DefaultEmptyArray var items: [String] = []

希望这对处理相同问题的人有所帮助。


更新:

在发布此答案并继续调查此事后,我发现了这个other article,但最重要的是相应的库包含一些常见的易于使用的@propertyWrappers 用于此类情况:

https://github.com/marksands/BetterCodable

【讨论】:

那么当对象中不再存在字段时,这对使用 Firestore Codable 有帮助吗? 是的,如果对象中缺少键,您可以根据类型创建一个默认为某个值的属性包装器。【参考方案6】:

如果您认为编写自己的init(from decoder: Decoder) 版本会让人不知所措,我建议您实现一种方法,在将输入发送到解码器之前检查输入。这样您就可以检查字段是否缺失并设置您自己的默认值。

例如:

final class CodableModel: Codable

    static func customDecode(_ obj: [String: Any]) -> CodableModel?
    
        var validatedDict = obj
        let someField = validatedDict[CodingKeys.someField.stringValue] ?? false
        validatedDict[CodingKeys.someField.stringValue] = someField

        guard
            let data = try? JSONSerialization.data(withJSONObject: validatedDict, options: .prettyPrinted),
            let model = try? CodableModel.decoder.decode(CodableModel.self, from: data) else 
                return nil
        

        return model
    

    //your coding keys, properties, etc.

为了从 json 中初始化一个对象,而不是:

do 
    let data = try JSONSerialization.data(withJSONObject: json, options: .prettyPrinted)
    let model = try CodableModel.decoder.decode(CodableModel.self, from: data)                        
 catch 
    assertionFailure(error.localizedDescription)

初始化将如下所示:

if let vuvVideoFile = PublicVideoFile.customDecode($0) 
    videos.append(vuvVideoFile)

在这种特殊情况下,我更喜欢处理可选项,但如果您有不同的意见,您可以让您的 customDecode(:) 方法可抛出

【讨论】:

【参考方案7】:

如果你不想实现你的编码和解码方法,那么围绕默认值有一些肮脏的解决方案。

您可以将新字段声明为隐式展开的可选字段,并在解码后检查它是否为 nil 并设置默认值。

我仅使用 PropertyListEncoder 对此进行了测试,但我认为 JSONDecoder 的工作方式相同。

【讨论】:

以上是关于使用 Swift 4 中的 JSONDecoder,缺少的键可以使用默认值而不是可选属性吗?的主要内容,如果未能解决你的问题,请参考以下文章

使用 Swift 4 中的 JSONDecoder,缺少的键可以使用默认值而不是可选属性吗?

如何在 swift 4 中使用 JSONDecoder 从嵌套 JSON 中获取数据?

Swift 4:JSONDecoder 在一种特定情况下失败-“操作无法完成”[关闭]

Swift 4 JSONDecoder 解码协议类型

如何在 swift 4.1 和 xcode 9.3 中使用 JSONDecoder 解码嵌套的 JSON 数组和对象?

在 Swift 4 中使用新的 JSONDecoder 协议时,我得到“无法读取数据,因为它的格式不正确。”错误