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

zek*_*kel 88 json swift swift4 codable

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
Run Code Online (Sandbox Code Playgroud)

Mar*_*n R 105

您可以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
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

您还可以创建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"
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

要么

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"
}
Run Code Online (Sandbox Code Playgroud)

重新评论:使用自定义扩展程序

extension KeyedDecodingContainer {
    func decodeWrapper<T>(key: K, defaultValue: T) throws -> T
        where T : Decodable {
        return try decodeIfPresent(T.self, forKey: key) ?? defaultValue
    }
}
Run Code Online (Sandbox Code Playgroud)

你可以实现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")
}
Run Code Online (Sandbox Code Playgroud)

但这并不比它短得多

    self.name = try container.decodeIfPresent(String.self, forKey: .name) ?? "Default Appleseed"
Run Code Online (Sandbox Code Playgroud)

  • 自动生成的方法无法从非选项中读取默认值,这仍然是荒谬的.我有8个选项和1个非可选项,所以现在手动编写编码器和解码器方法会带来很多样板.`ObjectMapper`处理得非常好. (42认同)
  • 当我们使用“codable”但仍然必须自定义 json 中缺少的键时,这真的很烦人:( (2认同)

小智 18

你可以实施。

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)
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 是的,这是最干净的答案,但是当你有大对象时它仍然会得到很多代码! (2认同)

Cri*_*tik 16

一种解决方案是,如果未找到JSON密钥,则使用默认为所需值的计算属性。这将增加一些额外的冗长性,因为您需要声明另一个属性,并且需要添加CodingKeys枚举(如果尚未存在)。优点是您无需编写自定义解码/编码代码。

例如:

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

    private var _name: String?

    enum CodingKeys: String, CodingKey {
        case _name = "name"
        case age
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 我最喜欢这个问题的答案。它允许我仍然使用默认的 JSONDecoder 并轻松地对一个变量进行例外处理。谢谢。 (2认同)

Leo*_*ver 9

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

struct MyClassDTO: Codable {
    let items: [String]?
    let otherVar: Int?
}
Run Code Online (Sandbox Code Playgroud)

然后,您只需使用该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)
    }
}
Run Code Online (Sandbox Code Playgroud)

这种方法也是很好的,因为您可以按自己的意愿重命名和更改最终对象。很明显,比手动解码需要更少的代码。此外,通过这种方法,您可以将网络层与其他应用程序分开。


lba*_*osa 7

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

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

我遇到了这篇文章,它展示了一种有趣的方法,可以在简单的情况下使用@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()
    }
}
Run Code Online (Sandbox Code Playgroud)

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

@DefaultEmptyArray var items: [String] = []
Run Code Online (Sandbox Code Playgroud)

希望这有助于有人处理同样的问题。


更新:

在继续调查此事的同时发布此答案后,我发现了另一篇文章,但最重要的是,相应的库包含一些常见的易于使用@propertyWrapper的此类情况:

https://github.com/marksands/BetterCodable