本文为 Raywenderlich 出版的《Expert Swift》第 8 章节《Codable》阅读笔记。

Swift 4.0 就引入了 Codable 协议。话不多说,进入正题:
示例 JSON:

{
  "accessKey" : "S|_|p3rs3cr37",
  "AddressInfo" : {
    "atmCode" : "1132",
    "city" : "Albuquerque",
    "street" : "3828 Piermont Drive",
    "zip" : 87112
  },
  "ContactInfo" : {
    "addedOn" : "04-14-2022 13:58",
    "cellularPhone" : "+972 542-288-482",
    "email" : "freak4pc@gmail.com",
    "homePhone" : "+1 212-741-4695",
    "website" : "http://github.com/freak4pc"
  },
  "family" : [
    "Tom",
    "Elia"
  ],
  "name" : "Shai Mishali"
}

如何 Decode?

即如何解析这段 JSON?

维持原有层级结构

这个 JSON 对应的 Swift 模型为:

import Foundation

struct Customer1: Codable {
    let accessKey: String
    let addressInfo: AddressInfo
    let contactInfo: ContactInfo
    let family: [String]
    let name: String

    enum CodingKeys: String, CodingKey {
        case accessKey
        case addressInfo
        case contactInfo
        case family, name
    }
}

// MARK: - AddressInfo
struct AddressInfo: Codable {
    let atmCode, city, street: String
    let zip: Int
}

// MARK: - ContactInfo
struct ContactInfo: Codable {
    let addedOn, cellularPhone, email, homePhone: String
    let website: String
}

Customer1 结构体模型的层级结构与 JSON 对象的层级结构保持一致。这样就可以利用 JSONDecoder 把示例 JSON 成功地解析为 Customer1 结构体模型。

自定义 JSON 对应模型的层级结构

以上述 Customer1 模型为例,如果想访问该顾客所在的城市:

let jack = Customer1()
let city = jack.addressInfo.city

如果想直接通过 jack.city 来访问 jack 所在的城市信息,该怎么处理?

1. 使用计算属性

    var city: String {
        return addressInfo.city
    }

但这种方式的弊端是:万一如果想把 AddressInfo 这嵌套的一层直接去掉,得手写好多类似上面的代码。

2. 使用嵌套的 CodingKey 枚举

使用嵌套的 CodingKey 枚举可以把具有层级的 JSON “拍扁”。

    enum CodingKeys: String, CodingKey {
        case name
        case accessKey
        case status
        case addressInfo = "AddressInfo"
        case contactInfo = "ContactInfo"
    }
    
    enum AddressInfo: String, CodingKey {
        case atmCode
        case street
        case city
        case zip
    }
    
    enum ContactInfo: String, CodingKey {
        case homePhone, cellularPhone, email, website
        case addedOn
    }

还得自己实现对应的 decode 方法:

    init(from decoder: Decoder) throws {
        let customer = try decoder.container(keyedBy: CodingKeys.self)
        let name = try customer.decode(String.self, forKey: .name)
        let accessKey = try customer.decode(String.self, forKey: .accessKey)
        let status = try customer.decode(Int.self, forKey: .status)
        self.name = name
        self.accessKey = accessKey
        
        let addressInfo = try customer.nestedContainer(keyedBy: AddressInfo.self, forKey: .addressInfo)
        let atmCode = try addressInfo.decode(String.self, forKey: .atmCode)
        let street = try addressInfo.decode(String.self, forKey: .street)
        let city = try addressInfo.decode(String.self, forKey: .city)
        
        // 当 JSON 对象中 `zip` key 对应的 value 类型是 String 时的兼容处理
        if case let zipString? = try? addressInfo.decodeIfPresent(String.self, forKey: .zip),
            let zipString = zipString
        {
            self.zip = Int(zipString) ?? -1
        } else if let zipInt = try addressInfo.decodeIfPresent(Int.self, forKey: .zip) {
            self.zip = zipInt
        } else {
            self.zip = -1
        }
        
        self.atmCode = atmCode
        self.street = street
        self.city = city
        
        let contactInfo = try customer.nestedContainer(keyedBy: ContactInfo.self, forKey: .contactInfo)
        let homePhone = try contactInfo.decode(String.self, forKey: .homePhone)
        let cellularPhone = try contactInfo.decode(String.self, forKey: .cellularPhone)
        let email = try contactInfo.decode(String.self, forKey: .email)
        let website = try contactInfo.decode(String.self, forKey: .website)
        let addedOn = try contactInfo.decode(Date.self, forKey: .addedOn)
        self.homePhone = homePhone
        self.cellularPhone = cellularPhone
        self.email = email
        self.website = website
        self.addedOn = addedOn
    }

在自己实现 decode 方法时,还需要对几类容器(Container)有了解:
JSON 示例中 Keyed、Nested、Unkeyed 和 Single Value 四类容器的位置

  • Keyed Container:最常见的容器之一,可用来解析键值对数据,对应的 key 定义在 CodingKeys 枚举中。
  • Unkeyed Container:用来解析不是以字符串为 key 的结构,例如常见的数组结构。
  • Single Value Container:用来把单一的数据解析为有具体类型的数据结构。
  • Nested Container:当某个容器成为另一个容器的子容器时,它被称为嵌套容器

我们可以方便地把容器理解为:一个对数据编解码的环境。

被“拍扁”后的模型为:

struct Customer2: Encodable {
    var name: String
    var accessKey: String
    var family: [String]
    
    var atmCode: String
    var street: String
    var city: String
    var zip: Int
    
    var homePhone: String
    var cellularPhone: String
    var email: String
    let website: String
    let addedOn = Date()

如何 Encode?

示例模型对象:

import Foundation

struct Customer: Encodable {
  var name: String
  var accessKey: String
  
  var atmCode: String
  var street: String
  var city: String
  var zip: Int
  
  var homePhone: String
  var cellularPhone: String
  var email: String
  let website: String
  let addedOn = Date()
  var family: [String]
}

let customer = Customer(
  name: "Shai Mishali",
  accessKey: "S|_|p3rs3cr37",
  atmCode: "1132",
  street: "3828 Piermont Drive",
  city: "Albuquerque",
  zip: 87119,
  homePhone: "+1 212-741-4695",
  cellularPhone: "+972 542-288-482",
  email: "freak4pc@gmail.com",
  website: "http://github.com/freak4pc",
  family: ["Tom", "Elia"]
)

我们可以利用 Encodable 的默认实现来把模型对象转为 JSON 对象:

let dateFormatter: DateFormatter = {
  let dateFormatter = DateFormatter()
  dateFormatter.dateFormat = "MM-dd-yyyy HH:mm"
  return dateFormatter
}()

let jsonEncoder = JSONEncoder()
jsonEncoder.dateEncodingStrategy = .formatted(dateFormatter)
jsonEncoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]

if let jsonObject = try? jsonEncoder.encode(customer),
let jsonString = String(data: jsonObject, encoding: .utf8) {
  print(jsonString)
}

// 输出:
{
  "accessKey" : "S|_|p3rs3cr37",
  "addedOn" : "04-14-2022 15:36",
  "atmCode" : "1132",
  "cellularPhone" : "+972 542-288-482",
  "city" : "Albuquerque",
  "email" : "freak4pc@gmail.com",
  "family" : [
    "Tom",
    "Elia"
  ],
  "homePhone" : "+1 212-741-4695",
  "name" : "Shai Mishali",
  "street" : "3828 Piermont Drive",
  "website" : "http://github.com/freak4pc",
  "zip" : 87119
}

修改 JSON 对象的层级结构

如果想修改 encode 之后的 JSON 对象的层级结构呢?还是得使用嵌套的 CodingKey 枚举,并自己实现 encode 方法:

    func encode(to encoder: Encoder) throws {
        var customer = encoder.container(keyedBy: CodingKeys.self)
        try customer.encode(name, forKey: .name)
        try customer.encode(accessKey, forKey: .accessKey)
        
        var addressInfo = customer.nestedContainer(keyedBy: AddressInfo.self, forKey: .addressInfo)
        try addressInfo.encode(atmCode, forKey: .atmCode)
        try addressInfo.encode(street, forKey: .street)
        try addressInfo.encode(city, forKey: .city)
        try addressInfo.encode(zip, forKey: .zip)
        
        var contactInfo = customer.nestedContainer(keyedBy: ContactInfo.self, forKey: .contactInfo)
        try contactInfo.encode(homePhone, forKey: .homePhone)
        try contactInfo.encode(cellularPhone, forKey: .cellularPhone)
        try contactInfo.encode(email, forKey: .email)
        try contactInfo.encode(website, forKey: .website)
        try contactInfo.encode(addedOn, forKey: .addedOn)
    }

达成 Codable

通过上述自定义 encode 和 decode 操作之后,就达成 Codable 。

import Foundation

struct Customer: Codable {
    internal init(name: String, accessKey: String, atmCode: String, street: String, city: String, zip: Int, homePhone: String, cellularPhone: String, email: String, website: String, addedOn: Date = Date(), family: [String]) {
        self.name = name
        self.accessKey = accessKey
        self.atmCode = atmCode
        self.street = street
        self.city = city
        self.zip = zip
        self.homePhone = homePhone
        self.cellularPhone = cellularPhone
        self.email = email
        self.website = website
        self.addedOn = addedOn
        self.family = family
    }
    
    var name: String
    var accessKey: String
    
    var atmCode: String
    var street: String
    var city: String
    var zip: Int
    
    var homePhone: String
    var cellularPhone: String
    var email: String
    let website: String
    var addedOn: Date = Date()
    var family: [String]
    
    enum CodingKeys: String, CodingKey {
        case name
        case accessKey
        case family
        case addressInfo = "AddressInfo"
        case contactInfo = "ContactInfo"
    }

    enum AddressInfo: String, CodingKey {
        case atmCode
        case street
        case city
        case zip
    }

    enum ContactInfo: String, CodingKey {
        case homePhone, cellularPhone, email, website
        case addedOn
    }
    
    func encode(to encoder: Encoder) throws {
        var customer = encoder.container(keyedBy: CodingKeys.self)
        try customer.encode(name, forKey: .name)
        try customer.encode(accessKey, forKey: .accessKey)
        
        var family = customer.nestedUnkeyedContainer(forKey: .family)
        try family.encode(contentsOf: self.family)

        var addressInfo = customer.nestedContainer(keyedBy: AddressInfo.self, forKey: .addressInfo)
        try addressInfo.encode(atmCode, forKey: .atmCode)
        try addressInfo.encode(street, forKey: .street)
        try addressInfo.encode(city, forKey: .city)
        try addressInfo.encode(zip, forKey: .zip)

        var contactInfo = customer.nestedContainer(keyedBy: ContactInfo.self, forKey: .contactInfo)
        try contactInfo.encode(homePhone, forKey: .homePhone)
        try contactInfo.encode(cellularPhone, forKey: .cellularPhone)
        try contactInfo.encode(email, forKey: .email)
        try contactInfo.encode(website, forKey: .website)
        try contactInfo.encode(addedOn, forKey: .addedOn)
    }
    
    init(from decoder: Decoder) throws {
        let customer = try decoder.container(keyedBy: CodingKeys.self)
        let name = try customer.decode(String.self, forKey: .name)
        let accessKey = try customer.decode(String.self, forKey: .accessKey)
        self.name = name
        self.accessKey = accessKey

        let addressInfo = try customer.nestedContainer(keyedBy: AddressInfo.self, forKey: .addressInfo)
        let atmCode = try addressInfo.decode(String.self, forKey: .atmCode)
        let street = try addressInfo.decode(String.self, forKey: .street)
        let city = try addressInfo.decode(String.self, forKey: .city)

        // 当 JSON 对象中 `zip` key 对应的 value 类型是 String 时的兼容处理
        if case let zipString? = try? addressInfo.decodeIfPresent(String.self, forKey: .zip),
            let zipString = zipString
        {
            self.zip = Int(zipString) ?? -1
        } else if let zipInt = try addressInfo.decodeIfPresent(Int.self, forKey: .zip) {
            self.zip = zipInt
        } else {
            self.zip = -1
        }

        self.atmCode = atmCode
        self.street = street
        self.city = city

        let contactInfo = try customer.nestedContainer(keyedBy: ContactInfo.self, forKey: .contactInfo)
        let homePhone = try contactInfo.decode(String.self, forKey: .homePhone)
        let cellularPhone = try contactInfo.decode(String.self, forKey: .cellularPhone)
        let email = try contactInfo.decode(String.self, forKey: .email)
        let website = try contactInfo.decode(String.self, forKey: .website)
        let addedOn = try contactInfo.decode(Date.self, forKey: .addedOn)
        self.homePhone = homePhone
        self.cellularPhone = cellularPhone
        self.email = email
        self.website = website
        self.addedOn = addedOn

        let family = try customer.decode([String].self, forKey: .family)
        self.family = family
    }
}

可以看出:自定义实现 encode 和 decode 方法所带来的代码量还是不小的,然而于此同时也带来了较高的灵活度。

举个例子,假如 JSON 对象中 zip 对应的值的类型是字符串,如“87119”。而 Customer 模型中 zip 变量类型为 Int,则可以在自定义 decode 方法中特别处理:

// 当 JSON 对象中 `zip` key 对应的 value 类型是 String 时的兼容处理
if case let zipString? = try? addressInfo.decodeIfPresent(String.self, forKey: .zip),
let zipString = zipString
{
    self.zip = Int(zipString) ?? -1
} else if let zipInt = try addressInfo.decodeIfPresent(Int.self, forKey: .zip) {
    self.zip = zipInt
} else {
    self.zip = -1
}

再者,如果 JSON 新加一个 memberType 字段来表示顾客的身份:

enum MemberType: Int {
    case visitor = 1
    case vip = 2
    case unknown = -1
}

突然某一天,收到的 JSON 对象中 memberType 为 3,意味超级 vip 客户,这是业务迭代新增的需求,而在 Customer 未对此改动作相应的更新时,也在 decode 方法里对这种情况作兼容处理,比如直接设为 .unknown

JSONEncoder

  • KeyEncodingStrategy:内置默认的 convertToSnakeCase 策略,如模型中的变量名为:xmlContents,经过 convertToSnakeCase 策略之后,对应的 JSON key 会变为 xml_contents。也提供自定义的接口,让开发者自行决定。
  • OutputFormatting:内置三种默认的输出格式化方式
    • prettyPrinted:iOS 7 开始支持
    • sortedKeys:iOS 11 开始支持
    • withoutEscapingSlashes:iOS 13 开始支持
  • userInfo:外部可通过此与自定义的 encode 过程进行信息传递。
  • DateEncodingStrategy:内置 iso8601 等编码格式,同时也提供传入自定义的 DateFormatter 对象对日期进行自定义处理。
  • DataEncodingStrategy:内置 base64 等编码格式,同时也提供自定义接口,让开发者自行决定。

JSONDecoder 的结构与 JSONEncoder 很相似,不再一一赘述。

SwiftyJSON 源码分析(未完待续)

SwiftyJSON 的 JSON 结构体组成及其输入和协议关系