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


114

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,它将提供默认值至少。
Aditya Sharma

Answers:


22

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

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


其他一些方法也能很好地工作,但最终我认为遵循这些原则的方法是最好的方法。
zekel

众所周知,但是代码重复过多。我更喜欢Martin R的答案
Kamen Dobrev

136

您可以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

@Hamish:我第一次尝试时它没有编译,但是现在可以了:)
Martin R

是啊,这是目前有点片状,而将固定(bugs.swift.org/browse/SR-5215
麦高

54
仍然很荒谬的是,自动生成的方法无法从非可选参数中读取默认值。我有8个可选选项和1个非可选选项,因此现在手动编写Encoder和Decoder方法将带来很多样板。ObjectMapper处理得很好。
Legoless

1
@LeoDabus可能是您遵循Decodable并提供了自己的实现init(from:)吗?在那种情况下,编译器假定您要自己手动处理解码,因此不会CodingKeys为您合成枚举。如您所说,遵循Codable而是可以工作,因为现在编译器正在encode(to:)为您进行综合,因此综合也可以CodingKeys。如果您还提供自己的实现encode(to:)CodingKeys将不再被综合。
Hamish

37

一种解决方案是,如果未找到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
    }
}

有趣的方法。它确实添加了一些代码,但是在创建对象之后,它非常清晰并且可以检查。
zekel

我最喜欢这个问题的答案。它使我仍然可以使用默认的JSONDecoder并轻松为一个变量设置异常。谢谢。
iOS_Mouse

注意:使用这种方法,您的属性将变为仅获取,因此无法直接将值分配给该属性。
甘帕特

8

您可以实施。

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

是的,这是最干净的答案,但是当您有大对象时,它仍然会获得很多代码!
Ashkan Ghodrat

1

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

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

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


0

如果您认为编写自己的版本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(:)方法可抛出


0

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

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

我碰到了这篇文章,展示了一种有趣的方法来克服简单情况下使用@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是,在这种情况下,包含一些易于使用的s 的相应库:

https://github.com/marksands/BetterCodable

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.