在 Swift 4中使用 JSONDecder 时,丢失的键可以使用默认值而不是可选属性吗?

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
67364 次浏览

您可以在您的类型中实现 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"

如果您认为编写您自己的 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)
}

Init 看起来像这样:

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

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

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

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

我只使用 PropertyListEncoder 测试了这一点,但我认为 JSONDecder 的工作原理是相同的。

如果找不到 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
}
}

如果希望属性 readwrite,还可以实现 setter:

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

这会增加一些额外的冗余,因为您需要声明另一个属性,并且需要添加 CodingKeys枚举(如果还没有的话)。这样做的好处是不需要编写定制的解码/编码代码,这在某种程度上可能会变得非常繁琐。

注意,只有当 JSON 键的值包含字符串或不存在时,此解决方案才有效。如果 JSON 可能在另一种形式下有值(例如,它是一个 int) ,那么您可以尝试 这个解决方案

你可以实施。

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)
}
}

我更喜欢的方法是使用所谓的 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)
}
}

这种方法也很好,因为您可以随心所欲地重命名和更改最终对象。 它是清楚的,需要比手动解码更少的代码。 此外,使用这种方法,您可以从其他应用程序分离网络层。

我遇到过这个问题,我也在寻找同样的答案。我找到的答案不是很令人满意,尽管我担心这里的解决方案将是唯一的选择。

在我的情况下,创建一个自定义解码器将需要大量的样板,将难以维护,所以我继续寻找其他答案。

我遇到了 这篇文章,它在使用 @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] = []

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


更新:

在发布了这个答案之后,我继续研究这个问题,我发现了这个 其他文章,但是最重要的是,它包含了一些常见的易于使用的 @propertyWrapper库:

Https://github.com/marksands/bettercodable