Encoding and Decoding in Swift笔记

原文:https://www.raywenderlich.com/3418439-encoding-and-decoding-in-swift

基础语法

Swift将 encoding和 decoding能力内置在了系统库(Codable协议)中,不需要像OC一样编写从JSON String 到 对象的decoding方法(及其逆方法),我们所需要做的只有:

  • 遵守Codable协议
struct Toy: Codable {
  var name: String
}
struct Employee: Codable {
  var name: String
  var id: Int
  var favoriteToy: Toy
}
  • 声明encode & 1. decoder
let encoder = JSONEncoder()
let decoder = JSONDecoder()

  • 执行编码和解码
let toy = Toy(name: "Teddy Bear")
let employee = Employee(name: "John Appleseed", id: 7, favoriteToy: toy)
let data = try encoder.encode(employee)
let string = String(data: data,  encoding: .utf8)
let sameEmployee = try decoder.decode(Employee.self, from: data)

Codable协议的能力

嵌套编码

一段用utf8编码过得JSON下发到客户端,实际上就相当于上述代码中的string,格式化处理后相当于:

{
  "name" : "John Appleseed",
  "id" : 7,
  "favoriteToy" : {
    "name" : "Teddy Bear"
  }
}

对于favoriteToy字段下嵌套的可以对应到Toy的JSON结构,由于Toy类型也遵守了Codable协议,也会被正确转为Toy对象

Snake Case 和 Camel Case之间的转换

可以帮助解决,客户端常用Camel Case 但 API可能使用Snake Case的问题:

  encoder.keyEncodeingStrategy = .convertToSnakeCase
  decoder.keyDecodingStratefgy = .convertFromSnakeCase

猜测内部会对key先做一层转换再去encode/decode

自定义JSON Key

可能出现客户端和API对key的定义不一样的情况,如API将favoriteToy改成了gift,客户端仍想保留变量名为favoriteToy,客户端可以选择自定义key值:

struct Employee: Codable {
  var name: String
  var id: Int
  var favoriteToy: Toy
  enum CodingKeys: String, CodingKey {
    case name, id, favoriteToy = "gift"
  }
}

需要注意的是,只有被列入CodingKeys枚举的变量才会被解析/编码,所以即使不需要自定义,需要的变量名仍要写进去

处理与客户端不同的JSON数据结构

Flat JSON

假如API把JSON都改成Flat的了:

{
  "name" : "John Appleseed",
  "id" : 7,
  "gift" : "Teddy Bear"
}

Employee数据结构不一样,客户端gift是一个Toy对象而非字符串,编译器无法自动生成,只能自己写decode(init)和encode方法了:

struct Toy: Codable {
  var name: String
}

struct Employee: Encodable {
  var name: String
  var id: Int
  var favoriteToy: Toy

  enum CodingKeys: CodingKey {
    case name, id, gift
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self) // 声明一个container,可以像Dictionory用指定键存储内容

    try container.encode(name, forKey: .name)
    try container.encode(id, forKey: .id)
    try container.encode(favoriteToy.name, forKey: .gift)  // 关键:指定key为gift去encode favoriteToy的内容
  }
}

// 写在extension里是为了保持Swift struct的member-wise initializer, 如果在struct的主定义里写了init方法就会导致失效
extension Employee: Decodable {
  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)

    name = try container.decode(String.self, forKey: .name)
    id = try container.decode(Int.self, forKey: .id)

    let gift = try container.decode(String.self, forKey: .gift)
    favoriteToy = Toy(name: gift)
  }
}

let toy = Toy(name: "Teddy Bear")
let employee = Employee(name: "John Appleseed", id: 7, favoriteToy: toy)

let encoder = JSONEncoder()
let decoder = JSONDecoder()

let data = try encoder.encode(employee)
let string = String(data: data, encoding: .utf8)!
let sameEmployee = try decoder.decode(Employee.self, from: data)
Deep JSON

假如API把JSON的数据结构又改成了这样:

{
  "name" : "John Appleseed",
  "id" : 7,
  "gift" : {
    "toy" : {
      "name" : "Teddy Bear"
    }
  }
}

又和Employee结构不一样,还是得像flat json的情况一样,重写encode和decode(init)方法,变化点:嵌套层加一个Keys枚举;嵌套层通过nestedContainer装入外层

struct Employee: Encodable {
  var name: String
  var id: Int
  var favoriteToy: Toy

  enum CodingKeys: CodingKey {
    case name, id, gift
  }

  enum GiftKeys: CodingKey {
    case toy
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(name, forKey: .name)
    try container.encode(id, forKey: .id)
    // 在外层container基础上声明嵌套container
    var giftContainer = container.nestedContainer(keyedBy: GiftKeys.self, forKey: .gift)
    try giftContainer.encode(favoriteToy, forKey: .toy)
  }
}

extension Employee: Decodable {
  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    name = try container.decode(String.self, forKey: .name)
    id = try container.decode(Int.self, forKey: .id)
    // 从外层container中获取嵌套container
    let giftContainer = try container.nestedContainer(keyedBy: GiftKeys.self, forKey: .gift)
    favoriteToy = try giftContainer.decode(Toy.self, forKey: .toy)
  }
}
Date

JSONEncoderJSONDecoder可以定制dateStrategy,系统提供了几种策略如secondsSince1970``millisecondsSince1970,默认使用deferredToDate。但日期的表示往往视实际项目而定,这时可以通过extension拓展DateFormatter

extension DateFormatter {
  static let dateFormatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.dateFormat = "dd-MM-yyyy"
    return formatter
  }()
}
// ......
encoder.dateEncodingStrategy = .formatted(.dateFormatter)
decoder.dateDecodingStrategy = .formatted(.dateFormatter)

.formatted策略只是按String的方式处理Date,DateEncodingStrategy的策略实际上有很多种,以应付各种格式的日期

/// The strategy to use for encoding `Date` values.
    public enum DateEncodingStrategy {

        /// Defer to `Date` for choosing an encoding. This is the default strategy.
        case deferredToDate

        /// Encode the `Date` as a UNIX timestamp (as a JSON number).
        case secondsSince1970

        /// Encode the `Date` as UNIX millisecond timestamp (as a JSON number).
        case millisecondsSince1970

        /// Encode the `Date` as an ISO-8601-formatted string (in RFC 3339 format).
        @available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
        case iso8601

        /// Encode the `Date` as a string formatted by the given formatter.
        case formatted(DateFormatter)

        /// Encode the `Date` as a custom value encoded by the given closure.
        ///
        /// If the closure fails to encode a value into the given encoder, the encoder will encode an empty automatic container in its place.
        case custom((Date, Encoder) throws -> Void)
    }

对于客户端来说,尤其是Decode,通常应该覆盖可能的各种格式,譬如:

decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in
  let decoder = JSONDecoder()
  decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in
      let container = try decoder.singleValueContainer()
      let dateStr = try container.decode(String.self)
      // possible date strings: "2016-05-01",  "2016-07-04T17:37:21.119229Z", "2018-05-20T15:00:00Z"
      let len = dateStr.count
      var date: Date? = nil
      if len == 10 {
          date = dateNoTimeFormatter.date(from: dateStr)
      } else if len == 20 {
          date = isoDateFormatter.date(from: dateStr)
      } else {
          date = self.serverFullDateFormatter.date(from: dateStr)
      }
      guard let date_ = date else {
          throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode date string \(dateStr)")
      }
      print("DATE DECODER \(dateStr) to \(date_)")
      return date_
  })
  return decoder
})
Subclasses

如果JSON数据结构是这样的:

{
  "toy" : {
    "name" : "Teddy Bear"
  },
  "employee" : {
    "name" : "John Appleseed",
    "id" : 7
  },
  "birthday" : 580794178.33482599
}

如果把employee字段包含的内容看做一个基础父类BasicEmployee,加入的toy和birthday其实是对BasicEmployee的拓展,当子类想利用父类已有的encode 和 decode方法:

class GiftEmployee: BasicEmployee {
  var birthday: Date
  var toy: Toy

  enum CodingKeys: CodingKey {
    case employee, birthday, toy
  }
  
  init(name: String, id: Int, birthday: Date, toy: Toy) {
    self.birthday = birthday
    self.toy = toy
    super.init(name: name, id: id)
  }

  required init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    birthday = try container.decode(Date.self, forKey: .birthday)
    toy = try container.decode(Toy.self, forKey: .toy)

    // 生成父类的decoder用于传给父类的init方法
    let baseDecoder = try container.superDecoder(forKey: .employee)
    try super.init(from: baseDecoder)
  }

  override func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(birthday, forKey: .birthday)
    try container.encode(toy, forKey: .toy)

    // 生成父类的encoder用于传给父类的encode方法
    let baseEncoder = container.superEncoder(forKey: .employee)
    try super.encode(to: baseEncoder)
  }
}

Handling Arrays With Mixed Types

如果JSON数据结构是这样的:

[
  {
    "name" : "John Appleseed",
    "id" : 7
  },
  {
    "id" : 7,
    "name" : "John Appleseed",
    "birthday" : 580797832.94787002,
    "toy" : {
      "name" : "Teddy Bear"
    }
  }
]

是一个混合了不同类型的数组,解决方案:

struct Toy: Codable {
  var name: String
}
let toy = Toy(name: "Teddy Bear")
let encoder = JSONEncoder()
let decoder = JSONDecoder()

enum AnyEmployee: Encodable {
  case defaultEmployee(String, Int)
  case customEmployee(String, Int, Date, Toy)
  case noEmployee

  enum CodingKeys: CodingKey {
    case name, id, birthday, toy
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)

    switch self {
    case .defaultEmployee(let name, let id):
      try container.encode(name, forKey: .name)
      try container.encode(id, forKey: .id)
    case .customEmployee(let name, let id, let birthday, let toy):
      try container.encode(name, forKey: .name)
      try container.encode(id, forKey: .id)
      try container.encode(birthday, forKey: .birthday)
      try container.encode(toy, forKey: .toy)
    default:
      let context = EncodingError.Context(codingPath: encoder.codingPath,
                                          debugDescription: "Invalid employee!")
      throw EncodingError.invalidValue(self, context)
    }
  }
}

extension AnyEmployee: Decodable {
  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    let containerKeys = Set(container.allKeys)
    let defaultKeys = Set<CodingKeys>([.name, .id])
    let customKeys = Set<CodingKeys>([.name, .id, .birthday, .toy])

    switch containerKeys {
    case defaultKeys:
      let name = try container.decode(String.self, forKey: .name)
      let id = try container.decode(Int.self, forKey: .id)
      self = .defaultEmployee(name, id)
    case customKeys:
      let name = try container.decode(String.self, forKey: .name)
      let id = try container.decode(Int.self, forKey: .id)
      let birthday = try container.decode(Date.self, forKey: .birthday)
      let toy = try container.decode(Toy.self, forKey: .toy)
      self = .customEmployee(name, id, birthday, toy)
    default:
      self = .noEmployee
    }
  }
}

let employees = [AnyEmployee.defaultEmployee("John Appleseed", 7),
                 AnyEmployee.customEmployee("John Appleseed", 7, Date(), toy)]
let employeesData = try encoder.encode(employees)
let employeesString = String(data: employeesData, encoding: .utf8)!
let sameEmployees = try decoder.decode([AnyEmployee].self, from: employeesData)

Array

如果JSON直接下发了一个没有key的数组,如:

[
  "teddy bear",
  "TEDDY BEAR",
  "Teddy Bear"
]

就需要用到unkeyedContainer

struct Toy: Codable {
  var name: String
}

let toy = Toy(name: "Teddy Bear")

let encoder = JSONEncoder()
let decoder = JSONDecoder()

struct Label: Encodable {
  var toy: Toy

  func encode(to encoder: Encoder) throws {
    var container = encoder.unkeyedContainer()
    try container.encode(toy.name.lowercased())
    try container.encode(toy.name.uppercased())
    try container.encode(toy.name)
  }
}

extension Label: Decodable {
  init(from decoder: Decoder) throws {
    var container = try decoder.unkeyedContainer()
    var name = ""
    while !container.isAtEnd {
      name = try container.decode(String.self)
    }
    toy = Toy(name: name)
  }
}

let label = Label(toy: toy)
let labelData = try encoder.encode(label)
let labelString = String(data: labelData, encoding: .utf8)!
let sameLabel = try decoder.decode(Label.self, from: labelData)

Array Within Objects

假如数据结构是这样:

{
  "name" : "Teddy Bear",
  "label" : [
    "teddy bear",
    "TEDDY BEAR",
    "Teddy Bear"
  ]
}

需要使用nestedUnkeyedContainer


let encoder = JSONEncoder()
let decoder = JSONDecoder()

struct Toy: Encodable {
  var name: String
  var label: String
  
  enum CodingKeys: CodingKey {
    case name, label
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(name, forKey: .name)
    var labelContainer = container.nestedUnkeyedContainer(forKey: .label)
    try labelContainer.encode(name.lowercased())
    try labelContainer.encode(name.uppercased())
    try labelContainer.encode(name)
  }
}

extension Toy: Decodable {
  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    name = try container.decode(String.self, forKey: .name)
    var labelContainer = try container.nestedUnkeyedContainer(forKey: .label)
    var labelName = ""
    while !labelContainer.isAtEnd {
      labelName = try labelContainer.decode(String.self)
    }
    label = labelName
  }
}

let toy = Toy(name: "Teddy Bear", label: "Teddy Bear")
let data = try encoder.encode(toy)
let string = String(data: data, encoding: .utf8)!
let sameToy = try decoder.decode(Toy.self, from: data)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269