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

46qrfjad  于 2023-01-06  发布在  Swift
关注(0)|答案(7)|浏览(144)

Swift 4添加了新的Codable协议,当我使用JSONDecoder时,它似乎要求Codable类的所有非可选属性在JSON中具有键,否则会抛出错误。
使类的每个属性都是可选的似乎是一个不必要的麻烦,因为我真正想要的是使用json中的值或默认值(我不希望属性为nil)。
有什么办法吗?

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
mlnl4t2r

mlnl4t2r1#

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

zwghvu4y2#

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

class MyCodable: Decodable {
    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枚举(如果还没有的话)。优点是你不需要编写自定义的解码/编码代码,这在某些时候会变得很乏味。
请注意,只有当JSON键的值为字符串或不存在时,此解决方案才有效。如果JSON可能具有其他形式的值(例如int),则可以尝试this solution

mwkjh3gx

mwkjh3gx3#

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

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

这种方法也很好,因为你可以重命名和改变最终的对象,无论你想。它是明确的,需要更少的代码比手动解码。此外,这种方法,你可以从其他应用程序分离网络层。

8qgya5xd

8qgya5xd4#

您可以实现。

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

nwwlzxa75#

我遇到这个问题是为了寻找完全相同的东西。我找到的答案不是很令人满意,尽管我担心这里的解决方案将是唯一的选择。
在我的情况下,创建一个自定义解码器将需要大量的样板文件,这将是很难维护的,所以我一直在寻找其他答案。
我遇到了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 s为这些类型的情况:
https://github.com/marksands/BetterCodable

x3naxklr

x3naxklr6#

如果您不想实现编码和解码方法,那么在默认值周围有一些不太好的解决方案。
您可以将新字段声明为隐式展开的可选字段,并在解码后检查它是否为nil,然后设置默认值。
我只用PropertyListEncoder测试了这一点,但我认为JSONDecoder的工作原理是一样的。

oyjwcjzk

oyjwcjzk7#

如果你觉得编写自己的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(:)方法设置为可抛出

相关问题