使用 Swift 4 中的 JSONDecoder,缺少的键是否可以使用默认值而不是必须是可选属性?
With JSONDecoder in Swift 4, can missing keys use a default value instead of having to be optional properties?
Swift 4 添加了新的 Codable
协议。当我使用 JSONDecoder
时,它似乎要求我的 Codable
class 的所有非可选属性在 JSON 中有键,否则它会抛出错误。
使我的 class 中的每个 属性 都可选似乎是一个不必要的麻烦,因为我真正想要的是使用 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(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)
}
初始化将如下所示:
if let vuvVideoFile = PublicVideoFile.customDecode([=12=]) {
videos.append(vuvVideoFile)
}
在这种特殊情况下,我更喜欢处理可选值,但如果您有不同意见,您可以使您的 customDecode(:) 方法可抛出
如果您不想实现您的编码和解码方法,默认值周围有一些不完善的解决方案。
您可以将新字段声明为隐式展开的可选字段,并在解码后检查它是否为 nil 并设置默认值。
我只使用 PropertyListEncoder 对此进行了测试,但我认为 JSONDecoder 的工作方式相同。
如果找不到 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
}
}
如果你想要 属性 读写,你也可以实现 setter:
var name: String {
get { _name ?? "Default Appleseed" }
set { _name = newValue }
}
这会增加一点额外的冗长,因为您需要声明另一个 属性,并且需要添加 CodingKeys
枚举(如果还没有的话)。优点是您不需要编写自定义 decoding/encoding 代码,这在某些时候可能会变得乏味。
请注意,此解决方案仅在 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)
}
}
这种方法也很好,因为您可以根据需要重命名和更改最终对象。
与手动解码相比,它清晰且需要的代码更少。
此外,通过这种方法,您可以将网络层与其他应用程序分开。
我在寻找完全相同的东西时遇到了这个问题。尽管我担心这里的解决方案将是唯一的选择,但我找到的答案并不是很令人满意。
就我而言,创建自定义解码器需要大量难以维护的样板文件,因此我一直在寻找其他答案。
我将 运行 转换为 this article,这显示了一种有趣的方法来在简单的情况下使用 @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] = []
希望这对处理相同问题的人有所帮助。
更新:
在继续调查此事的同时发布此答案后,我发现了这个 other article 但最重要的是,相应的库包含一些常见的易于使用的 @propertyWrapper
用于此类情况:
Swift 4 添加了新的 Codable
协议。当我使用 JSONDecoder
时,它似乎要求我的 Codable
class 的所有非可选属性在 JSON 中有键,否则它会抛出错误。
使我的 class 中的每个 属性 都可选似乎是一个不必要的麻烦,因为我真正想要的是使用 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(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)
}
初始化将如下所示:
if let vuvVideoFile = PublicVideoFile.customDecode([=12=]) {
videos.append(vuvVideoFile)
}
在这种特殊情况下,我更喜欢处理可选值,但如果您有不同意见,您可以使您的 customDecode(:) 方法可抛出
如果您不想实现您的编码和解码方法,默认值周围有一些不完善的解决方案。
您可以将新字段声明为隐式展开的可选字段,并在解码后检查它是否为 nil 并设置默认值。
我只使用 PropertyListEncoder 对此进行了测试,但我认为 JSONDecoder 的工作方式相同。
如果找不到 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
}
}
如果你想要 属性 读写,你也可以实现 setter:
var name: String {
get { _name ?? "Default Appleseed" }
set { _name = newValue }
}
这会增加一点额外的冗长,因为您需要声明另一个 属性,并且需要添加 CodingKeys
枚举(如果还没有的话)。优点是您不需要编写自定义 decoding/encoding 代码,这在某些时候可能会变得乏味。
请注意,此解决方案仅在 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)
}
}
这种方法也很好,因为您可以根据需要重命名和更改最终对象。 与手动解码相比,它清晰且需要的代码更少。 此外,通过这种方法,您可以将网络层与其他应用程序分开。
我在寻找完全相同的东西时遇到了这个问题。尽管我担心这里的解决方案将是唯一的选择,但我找到的答案并不是很令人满意。
就我而言,创建自定义解码器需要大量难以维护的样板文件,因此我一直在寻找其他答案。
我将 运行 转换为 this article,这显示了一种有趣的方法来在简单的情况下使用 @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] = []
希望这对处理相同问题的人有所帮助。
更新:
在继续调查此事的同时发布此答案后,我发现了这个 other article 但最重要的是,相应的库包含一些常见的易于使用的 @propertyWrapper
用于此类情况: