Codable 解析 JSON 配置默认值

2021-04-07 10:24:48 浏览数 (1)

2017年推出的 Codable 无疑是 Swift 的一大飞跃。尽管当时社区已经构建了多种用于本地 Swift 值和 JSON 之间 的编解码工具,但由于 Codable 与 Swift 编译器本身的集成,提供了前所未有的便利性,使我们能够通过使可解码类型遵守 Decodable 协议来定义可解码类型,例如:

代码语言:javascript复制
struct Article: Decodable {
    var title: String
    var body: String
    var isFeatured: Bool
}

然而,自从 Codable 引入以来,它就缺少了一个特性,那就是向某些属性添加默认值(而不必使它们成为可选的)。例如,假设上面的isFeatured属性并不总是出现在我们将从中解码文章实例的JSON数据中,在这种情况下,我们希望它默认为 false

即使我们将该默认值添加到属性声明本身,如果基础JSON 数据中缺少该值,则默认解码过程仍将失败:

代码语言:javascript复制
struct Article: Decodable {
    var title: String
    var body: String
    var isFeatured: Bool = false // 解码时并不会使用这个值
}

现在,我们总是编写自己的解码代码(通过重写init(from: Decoder) 的默认实现),但这将要求我们接管整个解码过程——这会破坏 Codable 的整个便利性,并要求我们不断更新该代码以应对模型属性的任何更改。

好消息是,我们可以采取另一种方法,那就是使用Swift的属性包装器功能,它使我们能够将自定义逻辑附加到任何存储的属性上。例如,我们可以使用该特性实现 DecodableBool 包装器,设置默认值为 false

代码语言:javascript复制
@propertyWrapper
struct DecodableBool {
    var wrappedValue = false
}

然后,我们可以使新的属性包装器实现Decodable协议,以使其能够“接管”它所附加的任何属性的解码过程。在这种情况下,我们确实要使用手动解码实现,因为这样可以直接从 Bool值中解码实例,如下所示:

代码语言:javascript复制
extension DecodableBool: Decodable {
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        wrappedValue = try container.decode(Bool.self)
    }
}

通过扩展实现 Decodable 协议的原因是这样写不会覆盖结构体的成员构造器。 简而言之就是直接写的话,DecodableBool的初始化器就变成了只有 init(from: Decoder),即: DecodableBool(from: Decoder) 而写在扩展的话不仅有init(from: Decoder),还有默认的便利构造器: DecodableBool() DecodableBool(wrappedValue: Bool) DecodableBool(from: Decoder) 参考 结构体便利初始化器 什么时候可以使用结构体的成员构造器?

最后,我们还需要 Codable在解码过程中将上述属性包装器的实例视为可选,这可以通过扩展KeyedDecodingContainer来重载解码特定的类型—— DecodableBool 来完成,在这种情况下,我们仅在存在值的情况下继续解码给定的键,否则我们将返回包装器的空实例:

代码语言:javascript复制
extension KeyedDecodingContainer {
    func decode(_ type: DecodableBool.Type,
                forKey key: Key) throws -> DecodableBool {
        try decodeIfPresent(type, forKey: key) ?? .init()
    }
}

有了上面的内容,我们现在可以简单地用新的DecodableBool属性注释任何Bool属性,并且在解码时它将默认设置为false

代码语言:javascript复制
struct Article: Decodable {
    var title: String
    var body: String
    @DecodableBool var isFeatured: Bool
}

非常好。但是,尽管我们现在已经解决了这个特定问题,但是我们的解决方案不是很灵活。如果在某些情况下希望将 true 设置为默认值,或者还要提供其他类型的默认解码值,我们该怎么办?

因此,让我们看看是否可以将解决方案推广到可以在更大范围的情况下应用的解决方案。为此,让我们从为默认源值(即需要解码的值)创建泛型协议开始——这将使我们能够定义各种默认值,而不仅仅是布尔值:

代码语言:javascript复制
protocol DecodableDefaultSource {
    associatedtype Value: Decodable
    static var defaultValue: Value { get }
}

然后,让我们使用一个枚举为即将编写的解码代码创建一个命名空间——这将为我们提供一个非常好的语法,并提供整洁的代码封装:

代码语言:javascript复制
enum DecodableDefault {}

使用无枚举值的枚举实现名称空间的优点是它们无法初始化,这使得它们充当纯包装器,而不是可以实例化的独立类型。

我们将添加到新命名空间的第一种类型是以前的DecodableBool属性包装器的泛型变体——现在它使用DecodableDefaultSource检索其默认wrappedValue,如下所示:

代码语言:javascript复制
extension DecodableDefault {
    @propertyWrapper
    struct Wrapper<Source: DecodableDefaultSource> {
        typealias Value = Source.Value
        var wrappedValue = Source.defaultValue
    }
}

接下来,让我们使上述属性包装器遵守Decodable,我们还将实现另一个特定新类型的KeyedDecodingContainer重载:

代码语言:javascript复制
extension DecodableDefault.Wrapper: Decodable {
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        wrappedValue = try container.decode(Value.self)
    }
}

extension KeyedDecodingContainer {
    func decode<T>(_ type: DecodableDefault.Wrapper<T>.Type,
                   forKey key: Key) throws -> DecodableDefault.Wrapper<T> {
        try decodeIfPresent(type, forKey: key) ?? .init()
    }
}

有了上述基础设施,现在让我们继续实现几个默认值源。我们将再次使用枚举为源代码提供额外级别的命名空间(就像Combine为其发布者提供的命名空间一样),并且我们还将添加一些类型别名以使代码更易于阅读:

代码语言:javascript复制
extension DecodableDefault {
    typealias Source = DecodableDefaultSource
    typealias List = Decodable & ExpressibleByArrayLiteral
    typealias Map = Decodable & ExpressibleByDictionaryLiteral

    enum Sources {
        enum True: Source {
            static var defaultValue: Bool { true }
        }

        enum False: Source {
            static var defaultValue: Bool { false }
        }

        enum EmptyString: Source {
            static var defaultValue: String { "" }
        }

        enum EmptyList<T: List>: Source {
            static var defaultValue: T { [] }
        }

        enum EmptyMap<T: Map>: Source {
            static var defaultValue: T { [:] }
        }
    }
}

通过将我们的 EmptyListEmptyMap 类型限制为 Swift 的两个文本协议,而不是ArrayDictionary这样的具体类型,我们可以涵盖更多的内容——因为许多不同的类型采用这些协议,包括SetIndexPath等等。

最后,让我们定义一系列方便类型别名,让我们将上述源代码引用为属性包装类型的专用版本——如下所示:

代码语言:javascript复制
extension DecodableDefault {
    typealias True = Wrapper<Sources.True>
    typealias False = Wrapper<Sources.False>
    typealias EmptyString = Wrapper<Sources.EmptyString>
    typealias EmptyList<T: List> = Wrapper<Sources.EmptyList<T>>
    typealias EmptyMap<T: Map> = Wrapper<Sources.EmptyMap<T>>
}

最后一部分为我们提供了一个非常好的语法,可以用可解码的默认值来注释属性,现在可以这样做:

代码语言:javascript复制
struct Article: Decodable {
    var title: String
    @DecodableDefault.EmptyString var body: String
    @DecodableDefault.False var isFeatured: Bool
    @DecodableDefault.True var isActive: Bool
    @DecodableDefault.EmptyList var comments: [Comment]
    @DecodableDefault.EmptyMap var flags: [String : Bool]
}

非常整洁,也许最好的部分是,我们的解决方案现在是真正的通用——我们可以很容易地添加新的来源,只要我们需要,同时保持我们的调用栈尽可能干净。

作为一系列的收尾工作,我们还将使用 Swift 的 条件一致性特征,使我们的属性包装器在其包装的值类型执行以下操作时符合常见协议,例如EquatablehashtableEncodable

代码语言:javascript复制
extension DecodableDefault.Wrapper: Equatable where Value: Equatable {}
extension DecodableDefault.Wrapper: Hashable where Value: Hashable {}

extension DecodableDefault.Wrapper: Encodable where Value: Encodable {
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(wrappedValue)
    }
}

有了它,我们现在有了一个完整的解决方案,可以用默认的值来注释属性——所有这些都不需要对正在解码的属性类型进行任何更改,而且由于我们的DecodableDefault枚举,它有一个整洁的封装实现。

感谢阅读!?

译自 John Sundell 的 Annotating properties with default decoding values

0 人点赞