Swift数据解析(第二篇) - Codable 下

1,740 阅读11分钟

这是Swift数据解析方案的系列文章:

Swift数据解析(第一篇) - 技术选型

Swift数据解析(第二篇) - Codable 上

Swift数据解析(第二篇) - Codable 下

Swift数据解析(第三篇) - Codable源码学习

Swift数据解析(第四篇) - SmartCodable 上

Swift数据解析(第四篇) - SmartCodable 下

七. 派生关系

1. super.init(from: decoder)

来看一个这样的场景,我们有一个 Point2D 的类,包含 x 和 y 两个属性,用来标记二维点的位置。 另有继承于 Point2D 的 Point3D,实现三维点定位。

现在我们将数据解析到 Point3D 中。

  class Point2D: Codable {
    var x = 0.0
    var y = 0.0
      
    enum CodingKeys: CodingKey {
        case x
        case y
    }
      
    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.x = try container.decode(Double.self, forKey: .x)
        self.y = try container.decode(Double.self, forKey: .y)
    }
  }
  
  class Point3D: Point2D {
    var z = 0.0
    
    enum CodingKeys: CodingKey {
        case z
        case point2D
    }
      
    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.z = try container.decode(Double.self, forKey: .z)
       
        try super.init(from: decoder)
    }
  }
  
  let json = """
  {
  "x" : 1,
  "y" : 2,
  "z" : 3
  }
  """
  
  guard let point = json.decode(type: Point3D.self) else { return }
  print(point.x)
  print(point.y)
  print(point.z)
  
  此时的打印结果是:
  1.0
  2.0
  3.0

2. super.encode(to: encoder)

我们再将模型编码

1. z 去哪里了?

  guard let value = point.encode() else { return }
  print(value)
  
  此时的打印结果是:
  {
  "x" : 1,
  "y" : 2
  }

相信你已经反应过来了:子类没有重写父类的 encode 方法,默认使用的父类的 encode 方法。子类的属性自然没有被 encode

2. x 和 y 的去哪了?

我们让 Point3D 实现override func encode(to encoder: Encoder) throws 方法

  class Point3D: Point2D {
    var z = 0.0
    
    enum CodingKeys: CodingKey {
        case z
        case point2D
    }
      
    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.z = try container.decode(Double.self, forKey: .z)
       
        try super.init(from: decoder)
    }
     
    override func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(self.z, forKey: .z)
    }
  }
  
  此时的打印结果是:
  {
  "z" : 3
  }

相信你应该非常清楚这个原因:子类重写了父类的 decode 实现,导致父类的 encode 没有执行。

3. x, y 和 z 都在了

调用父类的方法try super.encode(to: encoder) ,让父类也执行编码逻辑。

 override func encode(to encoder: Encoder) throws {
    try super.encode(to: encoder)
 ​
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(self.z, forKey: .z)
 }
  
  此时的打印结果是:
  {
  "x" : 1,
  "y" : 2,
  "z" : 3
  }

在子类的 encode 方法里面调用了父类的 encode 方法,完成了子类和父类的属性编码。

3. superEncoder & superDecoder

此时的数据

  {
  "x" : 1,
  "y" : 2,
  "z" : 3
  }

我们期望区分数据,x 和 y 是属于 Point2D的,z是属于Point3D的。

Point3Doverride func encode(to encoder: Encoder) throws 使用 container.superEncoder() 创建一个新的编码器。

 override func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(self.z, forKey: .z)
      
    let superEncoder = container.superEncoder()
    try super.encode(to: superEncoder)
 }
  
  输出信息是: 
  {
  "z" : 3,
  "super" : {
    "x" : 1,
    "y" : 2
  }
  }

mutating func superEncoder() -> Encoder 的定义是

为默认super密钥存储一个新的嵌套容器,并返回一个新的编码器实例,用于将super编码到该容器中。

如果是有键容器,相当于使用 Key(stringValue: "super", intValue: 0) 调用 superEncoder(forKey:).

如果是无键容器,相当于使用了Key(stringValue: "Index N", intValue: N) 调用 superEncoder(forKey:).

不要按照字面含义理解: ❌ 生成一个父编码器。

理解输出信息中的 super 的含义

我们来看一个示例:这是一个班级的信息,其包含班级号,班长,学生成员的信息。

  struct Student: Encodable {
    var name: String
    enum CodingKeys: CodingKey {
        case name
    }
  }
  
  struct Class: Encodable {
    var numer: Int
    var monitor: Student
    var students: [Student]
      
    init(numer: Int, monitor: Student, students: [Student]) {
        self.numer = numer
        self.monitor = monitor
        self.students = students
    }
  }
  
  let monitor = Student(name: "小明")
  let student1 = Student(name: "大黄")
  let student2 = Student(name: "小李")
  var classFeed = Class(numer: 10, monitor: monitor, students: [student1, student2])
  
  guard let value = classFeed.encode() else { return }
  print(value)
  
  // 输出信息是:
  {
  "numer" : 10,
  "monitor" : {
    "name" : "小明"
  },
  "students" : [
    {
      "name" : "大黄"
    },
    {
      "name" : "小李"
    }
  ]
  }

重写Class类的encode方法:

  func encode(to encoder: Encoder) throws {
      
  }
  
  // 报错信息:顶级类没有编码任何值
  Swift.EncodingError.Context(codingPath: [], debugDescription: "Top-level Class did not encode any values.", underlyingError: nil)

只创建容器:

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
  }
  
  // 输出信息是:
  {
  
  }

当前container的superEncoder

  enum CodingKeys: CodingKey {
    case numer
    case monitor
    case students
  }
  
  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
      
    var superEncoder = container.superEncoder()
  }
  
  // 输出信息是:
  {
  "super" : {
  
  }
  }

键值编码容器 和 无键编码容器的下 的区别

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
      
    // 在monitor字典容器中
    var monitorContainer = container.nestedContainer(keyedBy: Student.CodingKeys.self, forKey: .monitor)
    // 在monitor容器下,新生成一个编码器
    let monitorSuperEncoder = monitorContainer.superEncoder()
  
      
    // 在students数组容器中
    var studentsContainer = container.nestedUnkeyedContainer(forKey: .students)
    for student in students {
        let studentsSuperEncoder = studentsContainer.superEncoder()
    }
  }
  
  // 打印信息
  {
  "monitor" : {
    "super" : {
  
    }
  },
  "students" : [
    {
  
    },
    {
  
    }
  ]
  }

相信你已经体味到 super 的含义了。

4. 指定编码父类的key

系统还提供一个方法:

  public mutating func superEncoder(forKey key: KeyedEncodingContainer<K>.Key) -> Encoder

我们可以通过调用指定key的方法,创建一个新的编码器:

  class Point3D: Point2D {
    var z = 0.0
  
    enum CodingKeys: CodingKey {
        case z
        case point2D
    }
      
    override func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(self.z, forKey: .z)
          
        let superEncoder = container.superEncoder(forKey: .point2D)
        try super.encode(to: superEncoder)
    }
  }
  
  // 输出信息是:
  {
  "z" : 3,
  "point2D" : {
    "x" : 1,
    "y" : 2
  }
  }

当然我们也可以通过自定义CodingKey实现指定Key:

  class Point2D: Codable {
    var x = 0.0
    var y = 0.0
     
    struct CustomKeys: CodingKey {
        var stringValue: String
          
        init?(stringValue: String) {
            self.stringValue = stringValue
        }
          
        init(_ stringValue: String) {
            self.stringValue = stringValue
        }
          
        var intValue: Int?
          
        init?(intValue: Int) {
            self.stringValue = ""
        }
    }
  }
  
  class Point3D: Point2D {
    var z = 0.0
  
    enum CodingKeys: CodingKey {
        case z
    }
      
    override func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CustomKeys.self)
        try container.encode(self.z, forKey: CustomKeys.init("z"))
          
        let superEncoder = container.superEncoder(forKey: CustomKeys("point2D"))
        try super.encode(to: superEncoder)
    }
  }

八. 枚举

遵循Codable协议的枚举,也可以自动将数据转化为枚举值。

  struct EnumFeed: Codable {
    var a: SexEnum
    var b: SexEnum
  }
  
  enum SexEnum: String, Codable {
    case man
    case women
  }
  
  let json = """
  {
    "a": "man",
    "b": "women"
  }
  """
  
  guard let feed = json.decode(type: EnumFeed.self) else { return }
  print(feed

1. 枚举映射失败的异常

如果未被枚举的值出现(将数据中b的值改为 “unkown”),decode的时候会抛出DecodingError。 哪怕声明为可选,一样会报错。

   DecodingError
   dataCorrupted : Context
     codingPath : 1 element
      - 0 : CodingKeys(stringValue: "b", intValue: nil)
    - debugDescription : "Cannot initialize SexEnum from invalid String value unkown"
    - underlyingError : nil

2. 兼容方案

重写init方法,自定义映射

 struct EnumFeed: Codable {
    var a: SexEnum
    var b: SexEnum?
     
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.a = try container.decode(SexEnum.self, forKey: .a)
         
        if try container.decodeNil(forKey: .b) {
            self.b = nil
        } else {
            let bRawValue = try container.decode(String.self, forKey: .b)
            if let temp = SexEnum(rawValue: bRawValue) {
                self.b = temp
            } else {
                self.b = nil
            }
        }
    }
 }

使用协议提供映射失败的默认值

苹果官方给了一个解决办法: 使用协议提供映射失败的默认值

 public protocol SmartCaseDefaultable: RawRepresentable, Codable {
    /// 使用接收到的数据,无法用枚举类型中的任何值表示而导致解析失败,使用此默认值。
    static var defaultCase: Self { get }
 }
 
 public extension SmartCaseDefaultable where Self.RawValue: Decodable {
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let rawValue = try container.decode(RawValue.self)
        self = Self.init(rawValue: rawValue) ?? Self.defaultCase
    }
 }

使此枚举继承本协议即可

 enum SexEnum: String, SmartCaseDefaultable {
    case man
    case women
     
    static var defaultCase: SexEnum = .man
 }

九. 特殊格式的数据

1. 日期格式

 let json = """
 {
    "birth": "2000-01-01 00:00:01"
 }
 """
 guard let data = json.data(using: .utf8) else { return }
 let decoder = JSONDecoder()
 
 
 let dateFormatter = DateFormatter()
 dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
 decoder.dateDecodingStrategy = .formatted(dateFormatter)
 do {
    let feed = try decoder.decode(Person.self, from: data)
    print(feed)
 } catch {
    print("Error decoding: (error)")
 }
 
 struct Person: Codable {
    var birth: Date
 }

更多关于dateDecodingStrategy的使用请查看 DateDecodingStrategy

 public enum DateDecodingStrategy : Sendable {
 
    case deferredToDate
 
    case secondsSince1970
 
    case millisecondsSince1970
 
    case iso8601
    
    case formatted(DateFormatter)
 
    @preconcurrency case custom(@Sendable (_ decoder: Decoder) throws -> Date)
 }

2. 浮点数

 let json = """
 {
    "height": "NaN"
 }
 """
 
 struct Person: Codable {
    var height: Float
 }
 
 guard let data = json.data(using: .utf8) else { return }
 let decoder = JSONDecoder()
 decoder.nonConformingFloatDecodingStrategy = .convertFromString(positiveInfinity: "+∞", negativeInfinity: "-∞", nan: "NaN")
 do {
    let feed = try decoder.decode(Person.self, from: data)
    print(feed.height)
 } catch {
    print("Error decoding: (error)")
 }
 // 输出: nan

当Float类型遇到 NaN时候,如不做特殊处理,会导致解析失败。可以使用nonConformingFloatDecodingStrategy 兼容不符合json浮点数值当情况。

更多信息请查看:

 public enum NonConformingFloatDecodingStrategy : Sendable {
    case `throw`
    case convertFromString(positiveInfinity: String, negativeInfinity: String, nan: String)
 }

3. Data格式

有一个这样的base64的数据

 let json = """
 {
    "address": "aHR0cHM6Ly93d3cucWl4aW4uY29t"
 }
 """
 struct QXBWeb: Codable {
    var address: Data
 }
 
 guard let data = json.data(using: .utf8) else { return }
 let decoder = JSONDecoder()
 decoder.dataDecodingStrategy = .base64
 do {
    let web = try decoder.decode(QXBWeb.self, from: data)
    guard let address = String(data: web.address, encoding: .utf8) else { return }
    print(address)
 } catch {
    print("Error decoding: (error)")
 }
 // 输出: https://www.qixin.com

更多关于dataDecodingStrategy的信息,请查看DataDecodingStrategy。

 public enum DataDecodingStrategy : Sendable {
    case deferredToData
    /// 从base64编码的字符串解码' Data '。这是默认策略。
    case base64
    @preconcurrency case custom(@Sendable (_ decoder: Decoder) throws -> Data)
 }

4. URL格式

Codable可以自动将字符串映射为URL格式。

 struct QXBWeb: Codable {
    var address: URL
 }
 
 let json = """
 {
    "address": "https://www.qixin.com"
 }
 """
 
 guard let data = json.data(using: .utf8) else { return }
 let decoder = JSONDecoder()
 do {
    let web = try decoder.decode(QXBWeb.self, from: data)
    print(web.address.absoluteString)
 } catch {
    print("Error decoding: (error)")
 }

但是要注意 数据为 ""的情况。

  DecodingError
   dataCorrupted : Context
     codingPath : 1 element
      - 0 : CodingKeys(stringValue: "address", intValue: nil)
    - debugDescription : "Invalid URL string."
    - underlyingError : nil

我们来看系统内部是如何进行解码URL的:

 if T.self == URL.self || T.self == NSURL.self {
    guard let urlString = try self.unbox(value, as: String.self) else {
        return nil
    }
     
    guard let url = URL(string: urlString) else {
        throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath,
                                                                debugDescription: "Invalid URL string."))
    }
     
    decoded = (url as! T)
 }

解码URL分为两步: 1. urlString是否存在。 2. urlString是否可以转成URL。

可以使用这个方法兼容,URL没法提供默认值,只能设置为可选。

 struct QXBWeb: Codable {
    var address: URL?
     
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        do {
            let str = try container.decode(String.self, forKey: .address)
            if let url = URL(string: str) {
                self.address = url
            } else {
                self.address = nil
            }
        } catch {
            print(error)
            self.address = nil
        }
    }
 }

十. 关于嵌套结构

除了解码元素,UnkeyedDecodingContainerKeyedDecodingContainer 还提供了一些其他有用的方法,例如nestedContainer(keyedBy:forKey:)nestedUnkeyedContainer(forKey:),用于解码嵌套的容器类型。

  • nestedContainer: 用于生成解码字典类型的容器。
  • nestedUnkeyedContainer: 用于生成解码数组类型的容器。

nestedContainer的使用场景

在Swift中,nestedContainer是一种用于处理嵌套容器的方法。它是KeyedDecodingContainerUnkeyedDecodingContainer协议的一部分,用于解码嵌套的数据结构。

 ///返回存储在给定键类型的容器中的给定键的数据。
 ///
 /// -参数类型:用于容器的键类型。
 /// -参数key:嵌套容器关联的键。
 /// -返回:一个KeyedDecodingContainer容器视图。
 /// -抛出:' DecodingError. 'typeMismatch ',如果遇到的存储值不是键控容器。
 public func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey

当你需要解码一个嵌套的容器时,你可以使用nestedContainer方法。这个方法接受一个键(对于KeyedDecodingContainer)或者一个索引(对于UnkeyedDecodingContainer),然后返回一个新的嵌套容器,你可以使用它来解码嵌套的值。

数据结构对应图.png

我们将图左侧的数据,解码到图右侧的Class模型中。 Class 模型对应的数据结构如图所示。

我们先来创建这两个模型: Class 和 Student

 struct Student: Codable {
    let id: String
    let name: String
 }
 
 struct Class: Codable {
    var students: [Student] = []
 
    // 数据中的key结构和模型中key结构不对应,需要自定义Key
    struct CustomKeys: CodingKey {
        var intValue: Int? {return nil}
        var stringValue: String
        init?(intValue: Int) {return nil}
        init?(stringValue: String) {
            self.stringValue = stringValue
        }
         
        init(_ stringValue: String) {
            self.stringValue = stringValue
        }
    }
 }

Class是一个模型,所以对应的应该是一个KeyedDecodingContainer容器。

 init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CustomKeys.self)
 
    let keys = container.allKeys
 
    var list: [Student] = []
    for key in keys {
        let nestedContainer = try container.nestedContainer(keyedBy: CustomKeys.self, forKey: key)
        let name = try nestedContainer.decode(String.self, forKey: .init("name"))
        let student = Student(id: key.stringValue, name: name)
 
        list.append(student)
    }
    self.students = list
 }

container.allKeys的值为:

3 elements
  ▿ 0 : CustomKeys(stringValue: "2", intValue: nil)
    - stringValue : "2"1 : CustomKeys(stringValue: "1", intValue: nil)
    - stringValue : "1"2 : CustomKeys(stringValue: "3", intValue: nil)
    - stringValue : "3"

通过遍历allKeys得到的key,对应的数据结构是一个字典: { "name" : xxx }。

 let nestedContainer = try container.nestedContainer(keyedBy: CustomKeys.self, forKey: key)
 let name = try nestedContainer.decode(String.self, forKey: .init("name"))
 let student = Student(id: key.stringValue, name: name)

这样就得Student的全部数据。id的值就是key.stringValue,name的值就是nestedContainer容器中key为“name”的解码值。

根据这个思路,我们也可以很容易的完成 encode 方法

 func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CustomKeys.self)
     
    for student in students {
        var keyedContainer = container.nestedContainer(keyedBy: CustomKeys.self, forKey: .init(student.id))
        try keyedContainer.encode(student.name, forKey: .init("name"))
    }
 }

nestedUnkeyedContainer的使用场景

nestedUnkeyedContainer 是 Swift 中的 KeyedDecodingContainer 协议的一个方法,它允许你从嵌套的无键容器中解码一个值的数组,该容器通常是从 JSON 或其他编码数据中获取的。

 /// 返回为给定键存储的数据,以无键容器的形式表示。
 public func nestedUnkeyedContainer(forKey key: KeyedDecodingContainer<K>.Key) throws -> UnkeyedDecodingContainer

有一个这样的数据结构,解码到 Person 模型中:

 let json = """
 {
     "name": "xiaoming",
     "age": 10,
     "hobbies": [
         {
             "name": "basketball",
             "year": 8
         },
         {
             "name": "soccer",
             "year": 2
         }
     ]
 }
 """
 
 struct Person {
     let name: String
     let age: Int
     var hobbies: [Hobby]
     
     struct Hobby {
         let name: String
         let year: Int
     }
 }
使用系统自带嵌套解析能力
 struct Person: Codable {
    let name: String
    let age: Int
    var hobbies: [Hobby]
     
    struct Hobby: Codable {
        let name: String
        let year: Int
    }
 }
使用自定义解码
 struct Person: Codable {
     let name: String
     let age: Int
     var hobbies: [Hobby]
     
     struct Hobby: Codable {
         let name: String
         let year: Int
         
         enum CodingKeys: CodingKey {
             case name
             case year
         }
     }
     
     init(from decoder: Decoder) throws {
         let container = try decoder.container(keyedBy: CodingKeys.self)
         name = try container.decode(String.self, forKey: .name)
         age = try container.decode(Int.self, forKey: .age)
         
         // 解码嵌套的数组
         var nestedContainer = try container.nestedUnkeyedContainer(forKey: .hobbies)
         var tempHobbies: [Hobby] = []
         while !nestedContainer.isAtEnd {
             if let hobby = try? nestedContainer.decodeIfPresent(Hobby.self) {
                 tempHobbies.append(hobby)
             }
         }
         hobbies = tempHobbies
     }
 }
使用完全自定义解码
 struct Person: Codable {
     let name: String
     let age: Int
     var hobbies: [Hobby]
     
     struct Hobby: Codable {
         let name: String
         let year: Int
         
         enum CodingKeys: CodingKey {
             case name
             case year
         }
     }
     
     init(from decoder: Decoder) throws {
         let container = try decoder.container(keyedBy: CodingKeys.self)
         name = try container.decode(String.self, forKey: .name)
         age = try container.decode(Int.self, forKey: .age)
         
         var hobbiesContainer = try container.nestedUnkeyedContainer(forKey: .hobbies)
         var tempItems: [Hobby] = []
         while !hobbiesContainer.isAtEnd {
             let hobbyContainer = try hobbiesContainer.nestedContainer(keyedBy: Hobby.CodingKeys.self)
             
             let name = try hobbyContainer.decode(String.self, forKey: .name)
             let year = try hobbyContainer.decode(Int.self, forKey: .year)
             let item = Hobby(name: name, year: year)
             tempItems.append(item)
         }
         hobbies = tempItems
     }
 }

十一. 其他一些小知识点

1. 模型中属性的didSet方法不会执行

 struct Feed: Codable {
    init() {
         
    }
 
    var name: String = "" {
        didSet {
            print("被set了")
        }
    }
     
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.name = try container.decode(String.self, forKey: .name)
    }
 }
 
 let json = """
 {
    "name": "123"
 }
 """
 
 guard let feed = json.decode(type: Feed.self) else { return }
 print(feed)

在Swift中,使用Codable解析完成后,模型中的属性的didSet方法不会执行。

这是因为didSet方法只在属性被直接赋值时触发,而不是在解析过程中。

Codable协议使用编码和解码来将数据转换为模型对象,而不是通过属性的直接赋值来触发didSet方法。

如果您需要在解析完成后执行特定的操作,您可以在解析后手动调用相应的方法或者使用自定义的初始化方法来处理。