专栏首页韦弦的偶尔分享Codable 自定义解析 JSON

Codable 自定义解析 JSON

大多数现代应用程序的共同点是,它们需要对各种形式的数据进行编码或解码。无论是通过网络下载的JSON数据,还是存储在本地的模型的某种形式的序列化表示形式,对于几乎任何 Swift 代码库而言,能够可靠地编码和解码不同的数据都是必不可少的。

这就是为什么Swift的Codable API成为Swift 4.0的新功能一部分时具有如此重要的重要原因——从那时起,它已发展成为一种标准的,健壮的机制,可以在Apple的各种平台中使用编码和解码包括服务器端Swift。

Codable 之所以如此出色,是因为它与Swift工具链紧密集成,从而使编译器可以自动合成大量编码和解码各种值所需的代码。但是,有时我们确实需要自定义序列化时值的表示方式——因此,本周,让我们看一下可以调整Codable实现来做到这一点的几种不同方式。

修改 Key

让我们从一种基本的方式开始,我们可以通过修改用作序列化表示形式一部分的键来自定义类型的编码和解码方式。假设我们正在开发一款用于阅读文章的应用,而我们的一个核心数据模型如下所示:

struct Article: Codable {
    var url: URL
    var title: String
    var body: String
}

我们的模型当前使用完全自动合成的Codable实现,这意味着其所有序列化键都将匹配其属性的名称。但是,我们将从中解码Article值的数据(例如,从服务器下载的JSON)可能会使用略有不同的命名约定,从而导致默认解码失败。

幸运的是,这一问题很容易解决。要自定义Codable在解码(或编码)我们的Article类型的实例时将使用哪些键,我们要做的就是在其中定义一个CodingKeys枚举,并为与我们希望自定义的键匹配的大小写分配自定义原始值——像这样:

extension Article {
    enum CodingKeys: String, CodingKey {
        case url = "source_link"
        case title = "content_name"
        case body
    }
}

通过上述操作,我们可以继续利用编译器生成的默认实现进行实际的编码工作,同时仍使我们能够更改将用于序列化的键的名称。

虽然上面的技术非常适合当我们想要使用完全自定义的键名时,但是如果我们只希望Codable使用属性名的snake_case版本(例如,将backgroundColor转换为background_color),那么我们可以简单地更改JSON解码器的keyDecodingStrategy

var decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

以上两个API的优点在于,它们使我们能够解决Swift模型与用于表示它们的数据之间的不匹配问题,而无需我们修改属性名称。

忽略 Key

能够自定义编码键的名称确实很有用,但有时我们可能希望完全忽略某些键。例如,现在我们说我们正在开发一个记笔记应用程序,并且使用户能够将各种笔记分组在一起以形成一个可以包括本地草稿的NoteCollection

struct NoteCollection: Codable {
    var name: String
    var notes: [Note]
    var localDrafts = [Note]()
}

但是,虽然将localDrafts纳入NoteCollection模型确实很方便,但可以说,我们不希望在序列化或反序列化此类集合时包含这些草稿。这样做的原因可能是每次启动应用程序时为用户提供整洁的状态,或者是因为我们的服务器不支持草稿。

幸运的是,这也可以轻松完成,而不必更改NoteCollection的实际Codable实现。如果像以前一样定义一个CodingKeys枚举,而只是省略localDrafts,那么在对NoteCollection值进行编码或解码时,将不会考虑该属性:

extension NoteCollection {
    enum CodingKeys: CodingKey {
        case name
        case notes
    }
}

为了使以上功能正常运行,我们要省略的属性必须具有默认值——在这种情况下,localDrafts已经具有默认值。

创建匹配的结构

到目前为止,我们只是在调整类型的编码键——尽管这样做通常可以使您受益匪浅,但有时我们需要对Codable自定义进行进一步的调整。

假设我们正在构建一个包含货币换算功能的应用,并且正在将给定货币的当前汇率下载为 JSON 数据,如下所示:

{
    "currency": "PLN",
    "rates": {
        "USD": 3.76,
        "EUR": 4.24,
        "SEK": 0.41
    }
}

然后,在我们的Swift代码中,我们想要将此类JSON响应转换为CurrencyConversion实例——每个实例都包含一个ExchangeRate条目数组——每个币种对应一个:

struct CurrencyConversion {
    var currency: Currency
    var exchangeRates: [ExchangeRate]
}

struct ExchangeRate {
    let currency: Currency
    let rate: Double
}

但是,如果我们仅仅只是使以上两个模型都符合Codable,我们将再次导致Swift代码与我们要解码的JSON数据不匹配。但是这次,不只是关键字名称的问题——结构上有根本的不同。

当然,我们可以修改Swift模型的结构,使其与JSON数据的结构完全匹配,但这并不总是可行的。尽管拥有正确的序列化代码很重要,但是拥有适合我们实际代码库的模型结构也同样重要。

相反,让我们创建一个新的专用类型——它将在JSON数据中使用的格式与Swift代码的结构体之间架起一座桥梁。在这种类型中,我们将能够封装将JSON汇率字典转换为一系列ExchangeRate模型所需的所有逻辑,如下所示:

private extension ExchangeRate {
    struct List: Decodable {
        let values: [ExchangeRate]

        init(from decoder: Decoder) throws {
            let container = try decoder.singleValueContainer()
            let dictionary = try container.decode([String : Double].self)

            values = dictionary.map { key, value in
                ExchangeRate(currency: Currency(key), rate: value)
            }
        }
    }
}

使用上述类型,我们现在可以定义一个私有属性,该名称与用于其数据的JSON密钥相匹配——并使我们的exchangeRates属性仅充当该私有属性的面向公众的代理:

struct CurrencyConversion: Decodable {
    var currency: Currency
    var exchangeRates: [ExchangeRate] {
        return rates.values
    }
    
    private var rates: ExchangeRate.List
}

上面的方法起作用的原因是,在对值进行编码或解码时,永远不会考虑计算属性。

当我们想使我们的Swift代码与使用非常不同的结构的JSON API兼容时,上述技术可能是一个很好的工具——且无需完全从头实现Codable

转换值

在解码时,尤其是在使用我们无法控制的外部JSON API进行解码时,一个非常常见的问题是,以与Swift的严格类型系统不兼容的方式对类型进行编码。例如,我们要解码的JSON数据可能使用字符串来表示整数或其他类型的数字。

让我们来看看一种可以让我们处理这些值的方法,再次以一种自包含的方式,它不需要我们编写完全自定义的Codable实现。

我们本质上想要做的是将字符串值转换为另一种类型,以Int为例。我们将从定义一个协议开始,该协议使我们可以将任何类型都标记为StringRepresentable,这意味着可以将其转换为字符串表示形式,也可以将其从字符串表示形式转换为我们要的类型:

struct StringBacked<Value: StringRepresentable>: Codable {
    var value: Value
    
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let string = try container.decode(String.self)
        
        guard let value = Value(string) else {
            throw DecodingError.dataCorruptedError(
                in: container,
                debugDescription: "Failed to convert an instance of \(Value.self) from '\(string)'"
            )
        }
        
        self.value = value
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(value.description)
    }
}

就像我们以前为兼容JSON的基础存储创建私有属性的方式一样,现在我们可以对编码后由字符串后端的任何属性执行相同的操作,同时仍将数据适当地公开给其他Swift代码类型。这是一个针对视频类型的numberOfLikes属性执行此操作的示例:

struct Video: Codable {
    var title: String
    var description: String
    var url: URL
    var thumbnailImageURL: URL
    
    var numberOfLikes: Int {
        get { return likes.value }
        set { likes.value = newValue }
    }
    
    private var likes: StringBacked<Int>
}

在必须手动为属性定义settergetter的复杂性与必须回退到完全自定义的Codable实现的复杂性之间,这里肯定有一个折中——但是对于上述Video 结构体这样的类型,它在其中仅具有一个属性需要自定义,使用私有支持属性可能是一个不错的选择。

结语

尽管编译器能够自动合成不需要任何形式的自定义的所有类型的Codable支持,这真是太棒了,但是我们能够在需要时进行自定义,这一事实同样是太棒了。

更好的是,这样做实际上并不需要我们完全放弃自动生成的代码,而是采用手动实现——很多时候,可以稍微调整类型的编码或解码方式,同时仍然让编译器做大部分繁重的工作。

谢谢阅读!

译自 John Sundell 的 Customizing Codable types in Swift

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Hacking with iOS: SwiftUI Edition - 里程碑:项目 10 - 12

    这最后三个项目确实推动了数据的开发,首先是通过互联网发送和接收数据,然后进入Core Data,以便您可以了解实际应用如何管理其数据。您在此项目中学到的技能也许...

    韦弦zhy
  • propertye wrapped, optional在Swift妙用

    第三种的解包造成大量的'??', 对于接触一段时间swift就知道上面age的声明内部其实是一个Optional的类型,等价于:

    大话swift
  • 30.Swift学习之Codable协议

    开发中推荐使用Paste JSON as Code • quicktype软件,可以根据JSON快速生成Model文件

    YungFan
  • Encoding and Decoding Custom Types

    许多编程任务涉及通过网络连接发送数据,将数据保存到磁盘或将数据提交到API和服务。 这些任务通常要求在传输数据时将数据编码和解码为中间格式。

    SheltonWan
  • Swift 项目中涉及到 JSONDecoder,网络请求,泛型协议式编程的一些记录和想法

    最近项目开发一直在使用 swift,因为 HTN 项目最近会有另外一位同事加入,所以打算对最近涉及到的一些技术和自己的一些想法做个记录,同时也能够方便同事熟悉代...

    用户7451029
  • Codable 解析 JSON 忽略无效的元素

    默认情况下,使用 Swift 内置的 Codable API 编码或解码数组只有全部成功或者全部失败两种情况。可以成功处理所有元素,或者引发错误,这可以说是一个...

    韦弦zhy
  • Swift:处理多级Codable数据

    Codable协议使处理简单的单层数据变得容易:如果您处理的是一个类型的单个实例,或者这些实例的数组或字典,那么一切就正常了。但是,在此项目中,我们将处理稍微复...

    韦弦zhy
  • Hacking with iOS: SwiftUI Edition - Moonshot 项目(一)

    在这个项目中,我们将构建一个应用程序,让用户了解组成美国宇航局阿波罗太空计划的任务和宇航员。您将获得更多的使用Codable的经验,但更重要的是,您还将使用滚动...

    韦弦zhy
  • 使用Codable归档Swift对象

    UserDefaults非常适合存储简单的设置,例如整数和布尔值,但是当涉及复杂数据时——例如自定义Swift类型——我们需要做更多的工作。

    韦弦zhy

扫码关注云+社区

领取腾讯云代金券