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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/44575293/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-03 18:55:46  来源:igfitidea点击:

With JSONDecoder in Swift 4, can missing keys use a default value instead of having to be optional properties?

jsonswiftswift4codable

提问by zekel

Swift 4 added the new Codableprotocol. When I use JSONDecoderit seems to require all the non-optional properties of my Codableclass to have keys in the JSON or it throws an error.

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

Making every property of my class optional seems like an unnecessary hassle since what I really want is to use the value in the json or a default value. (I don't want the property to be nil.)

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

Is there a way to do this?

有没有办法做到这一点?

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

采纳答案by Leonid Silver

Approach that I prefer is using so called DTOs - data transfer object. It is a struct, that conforms to Codable and represents the desired object.

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

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

Then you simply init the object that you want to use in the app with that DTO.

然后,您只需使用该 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)
    }
}

This approach is also good since you can rename and change final object however you wish to. It is clear and requires less code than manual decoding. Moreover, with this approach you can separate networking layer from other app.

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

回答by Martin R

You can implement the init(from decoder: Decoder)method in your type instead of using the default implementation:

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

You can also make namea constant property (if you want to):

您还可以创建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"
        }
    }
}

or

或者

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

Re your comment:With a custom extension

回复您的评论:使用自定义扩展

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

you could implement the init method as

您可以将 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")
}

but that is not much shorter than

但这并不比

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

回答by Cristik

One solution would be to use a computed property that defaults to the desired value if the JSON key is not found. This adds some extra verbosity as you'll need to declare another property, and will require adding the CodingKeysenum (if not already there). The advantage is that you don't need to write custom decoding/encoding code.

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

For example:

例如:

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

回答by Ankit

You can implement.

你可以实施。

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

回答by Kirill Kuzyk

If you don't want to implement your encoding and decoding methods, there is somewhat dirty solution around default values.

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

You can declare your new field as implicitly unwrapped optional and check if it's nil after decoding and set a default value.

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

I tested this only with PropertyListEncoder, but I think JSONDecoder works the same way.

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

回答by Eugene Alexeev

If you think that writing your own version of init(from decoder: Decoder)is overwhelming, I would advice you to implement a method which will check the input before sending it to decoder. That way you'll have a place where you can check for fields absence and set your own default values.

如果您认为编写自己的init(from decoder: Decoder). 这样,您将有一个地方可以检查字段缺失并设置自己的默认值。

For example:

例如:

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

And in order to init an object from json, instead of:

为了从 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 will look like this:

Init 将如下所示:

if let vuvVideoFile = PublicVideoFile.customDecode(##代码##) {
    videos.append(vuvVideoFile)
}

In this particular situation I prefer to deal with optionals but if you have a different opinion, you can make your customDecode(:) method throwable

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