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

使用Swift 4中的JSONDecoder,丢失的键是否可以使用默认值,而不必是可选属性?,json,swift,swift4,codable,Json,Swift,Swift4,Codable,Swift 4增加了新的Codable协议。当我使用JSONDecoder时,它似乎要求我的Codable类的所有非可选属性在JSON中都有键,否则它会抛出一个错误 使类的每个属性都可选似乎是一个不必要的麻烦,因为我真正想要的是使用json中的值或默认值。(我不希望属性为零。) 有办法做到这一点吗 class MyCodable: Codable { var name: String = "Default Appleseed" } func load(input: String) {

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

您可以在您的类型中实现
init(来自解码器:解码器)
方法,而不是使用默认实现:

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
设置为常量属性(如果需要):

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

extension KeyedDecodingContainer {
    func decodeWrapper<T>(key: K, defaultValue: T) throws -> T
        where T : Decodable {
        return try decodeIfPresent(T.self, forKey: key) ?? defaultValue
    }
}
但这并不比

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

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

在这种特殊情况下,我更喜欢处理optionals,但如果您有不同的意见,可以将customDecode(:)方法设置为可丢弃的

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

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


我只使用PropertyListEncoder测试了这一点,但我认为JSONDecoder的工作方式是相同的。

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

class MyCodable:Codable{
变量名称:字符串{return\u name??“Default Appleseed”}
变量年龄:Int?
//这是实际解码/编码的属性
私有变量名称:字符串?
枚举编码键:字符串,编码键{
案例_name=“name”
病例年龄
}
}
如果要使属性readwrite,还可以实现setter:

var name: String {
    get { _name ?? "Default Appleseed" }
    set { _name = newValue }
}
这会增加一点额外的冗余,因为您需要声明另一个属性,并且需要添加
CodingKeys
enum(如果还没有)。优点是您不需要编写定制的解码/编码代码,这在某些时候可能会变得单调乏味

请注意,此解决方案仅在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)
    }
}

我更喜欢的方法是使用所谓的DTOs—数据传输对象。 它是一个符合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而不失败,但也显示了其他不同的变体。 您可以更详细地阅读它,但我将展示我为我的用例所做的工作

在我的例子中,我有一个
数组
,如果缺少密钥,我希望将其初始化为空

因此,我声明了以下
@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
s,适用于此类情况:


还请注意,在这种特殊情况下,您可以使用自动生成的
CodingKeys
枚举(这样可以删除自定义定义):@Hamish:我第一次尝试时它没有编译,但现在它可以工作:)是的,它目前有点不完整,但将被修复()自动生成的方法无法从非可选项读取默认值,这仍然很荒谬。我有8个可选项和1个非可选项,所以现在手动编写编码器和解码器方法会带来很多样板
ObjectMapper
很好地处理了这个问题。@LeoDabus可能是因为您遵守了
Decodable
,并且还提供了自己的
init(from:)
?在这种情况下,编译器假定您希望自己手动处理解码,因此不会为您合成
CodingKeys
enum。正如您所说,遵从
Codable
反而有效,因为现在编译器正在为您合成
encode(to:)
,因此也合成
CodingKeys
。如果您还提供自己的
encode(to:)
CodingKeys
将不再被合成。还有一个问题,如果我的json中有多个键,并且我想编写一个通用方法来映射json以创建对象,而不是给出nil,那么它应该至少给出默认值
struct MyClassDTO: Codable {
    let items: [String]?
    let otherVar: Int?
}
 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
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()
    }
}
@DefaultEmptyArray var items: [String] = []