13、Swift中的Codable

1 Codable简介

Codable 是 Swift 4.0 以后推出的一个编解码协议,可以配合 JSONDecoder 和 JSONEncoder 用来进行 JSON 解码和编码。

typealias Codable = Decodable & Encodable

public protocol Encodable {
    public func encode(to encoder: Encoder) throws
}

public protocol Decodable {
    public init(from decoder: Decoder) throws
}

Decodable 和 Encodable 是需要使用的两个实际协议。但是,为了方便起见,我们通常使用 Codable 别名来处理 JSON 编码和解码。

2 Codable的痛点

只要有一个属性解析失败则直接抛出异常导致整个解析过程失败。

以下情况均会解析失败:

  • 类型不匹配,例如 APP 端是 Int 类型,服务器下发的是 String 类型
  • 不可选类型键不存在, 例如服务器下发的数据缺少了某个字段
  • 不可选类型值为 null,例如服务器由于某种原因导致数据为 null

后两个可以通过使用可选类型避免,第一种情况只能重写协议方法来规避,但是很难完全避免。而使用可选类型势必会有大量的可选绑定,对于 enum 和 Bool 来说使用可选类型是非常痛苦的,而且这些都会增加代码量。这时候就需要一种解决方案来解决这些痛点。

3 Codable的简单使用

声明一个JSON变量。

let json = """
    {
        "firstName": "lichangan",
        "age": 200,
        "description": "A handsome boy."
    }
"""

建立一个模型

struct Person : Codable {
    var firstName:String
    var age:Int
    var description:String
}

JSON转Model

let decoder = JSONDecoder()
if let jsonData = json.data(using: .utf8) {
    do {
        let person = try decoder.decode(Person.self, from: jsonData)
        print(person.firstName)
    }catch {
        print("JSON解析失败")
    }
}

如果后台返回的JSON的数据结构比较复杂嵌套的时候,我们可以在struct 里再套一个 struct 来建立模型。

4、泛型函数来进行encode和decode

func encode<T>(of model: T) throws ->Data where T: Codable {
    let encoder = JSONEncoder()
    encoder.outputFormatting = .prettyPrinted
    let encodedData = try encoder.encode(model)
    return encodedData
}
func decode<T>(of jsonString: String, type: T.Type) throws -> T where T: Codable {
    let data = jsonString.data(using: .utf8)!
    let decoder = JSONDecoder()
    let model = try decoder.decode(T.self, from: data)
    return model
}

5 、json中的key和模型中的Key不对应

但有时后台系统使用的命名规则有可能与前端不一致,比如后台字段返回下划线命名法,而一般我们使用驼峰命名法,所以在字段映射的时候就需要修改一下,例如后台返回first_name而不是firstName。

这里有两种解决:

5.1 实现CodingKey协议进行枚举映射

let json = """
    {
        "first_name": "lichangan",
        "age": 200,
        "description": "A handsome boy."
    }
"""
struct Person : Codable {
    var name:String
    var age:Int
    var description:String
    // 自定义字段属性
    // 注意 1.需要遵守Codingkey  2.每个字段都要枚举
    private enum CodingKeys:String,CodingKey {
        case name = "first_name"
        case age
        case description
    }
}

//JSON转Model
let decoder = JSONDecoder()
if let jsonData = json.data(using: .utf8) {
    do {
        let person = try decoder.decode(Person.self, from: jsonData)
        print(person.name)
    }catch {
        print("JSON解析失败")
    }
}

5.2 keyDecodingStrategy

通过Decoder的keyDecodingStrategy属性, 将属性的值设置为convertFromSnakeCase,就是将编码策略设置为可将驼峰命名法转化为下划线,这样我们就不需要写额外的代码处理了,该属性是Swift4.1之后推出的。

//定义JSON数据
let json = """
    {
        "first_name": "lichangan",
        "age": 200,
        "description": "A handsome boy."
    }
"""
struct Person : Codable {
    var firstName:String
    var age:Int
    var description:String
}
//JSON转Model
let decoder = JSONDecoder()
// 编码策略  使用从蛇形转化为大写 encode时同样也可将驼峰命名法转化为下划线
decoder.keyDecodingStrategy = .convertFromSnakeCase
if let jsonData = json.data(using: .utf8) {
    do {
        let person = try decoder.decode(Person.self, from: jsonData)
        print(person.firstName)
    }catch {
        print("JSON解析失败")
    }
}

keyDecodingStrategy是一个枚举值,他还提供了自定义转化规则

public enum KeyDecodingStrategy {
        case useDefaultKeys
        case convertFromSnakeCase
        case custom(([CodingKey]) -> CodingKey) //自定义转化规则
  }
let json = """
    {
        "First_name": "米饭童鞋",
        "Points": 200,
        "Pescription": "A handsome boy."
    }
"""
let decoder = JSONDecoder()
// 自定义转化规则
 decoder.keyDecodingStrategy = .custom({ (keys) -> CodingKey in
             let lastKey = keys.last!
             guard lastKey.intValue == nil else { return lastKey }
             // 将首字母大写的转化为小写的
             let stringValue = lastKey.stringValue.prefix(1).lowercased()    +lastKey.stringValue.dropFirst()
             return AnyKey(stringValue: stringValue)!
  })

6、处理常见的JSON嵌套结构

6.1 使用嵌套的JSON对象


let json = """
{
    "name": "lichangan",
    "location": {
        "country": "China"
    },
    "use": "to buy a new collection of clothes to stock her shop before the holidays.",
    "loan_amount": 150
}
"""
struct Loan:Codable {
    var name:String?
    var country:String?
    var use:String?
    var amount:Int?
    
    enum CodingKeys:String,CodingKey {
        case name
        case country = "location"
        case use
        case amount = "loan_amount"
    }
    
    enum LocationKeys : String,CodingKey {
        case country
    }
    
    init(from decoder: Decoder) throws {
        do {
            let values = try decoder.container(keyedBy: CodingKeys.self)
            
            name = try values.decode(String.self, forKey: .name)
            
            let location = try values.nestedContainer(keyedBy: LocationKeys.self, forKey: .country)
            country = try location.decode(String.self, forKey: .country)
            
            use = try values.decode(String.self, forKey: .use)
            
            amount = try values.decode(Int.self, forKey: .amount)
            
        }catch {
            print(error.localizedDescription)
        }
        
    }
}

if let jsonData = json.data(using: .utf8){
    let decoder = JSONDecoder()
    do {
        let loan = try decoder.decode(Loan.self, from: jsonData)
        print(loan.name)
        print(loan.country)
        print(loan.use)
        print(loan.amount)
    }catch {
        print(error.localizedDescription)
    }
}

6.2 处理Enum

let json = """
{
    "name": "lichangan",
    "sex":1,
    "use": "to buy a new collection of clothes to stock her shop before the holidays.",
    "loan_amount": 150
   
}
"""
enum Sex:Int,Codable {
    case unknow = 1 //未知
    case man = 2 //男
    case female = 3 //女
}

struct Person : Codable {
    var name:String
    var sex:Sex
    var use:String
    var amount:Int
    
    enum CodingKeys:String,CodingKey {
        case name
        case sex
        case use
        case amount = "loan_amount"
    }
}

if let jsonData = json.data(using: .utf8) {
    let decoder = JSONDecoder()
    do {
        let person = try decoder.decode(Person.self, from: jsonData)
        print(person.sex)
    }catch{
        print(error.localizedDescription)
    }
}

但是这样做要求服务端返回的状态码,必须是客户端已枚举的。假如返回的状态码不是已知的:
比如服务器性别返回0,那么decode 这一步就会得到一个 DecodingError,类型是 .dataCorrupted,描述是:

Cannot initialize Code from invalid String value account_not_exist*

也就是说,只要是未知的字符串,我们都无法解析。甚至还需要根据 DecodingError 提供的 context 来单独处理这种情况。

有后备 case 的 Enum

或者我们也可以让 Sex这个 Enum 自动处理未知情况。
声明一个协议,叫 CodableEnumeration:

protocol CodableEnumeration: RawRepresentable, Codable where RawValue: Codable {
    static var defaultCase: Self { get }
}

通过 extension,让协议提供默认 Decode 方法:

extension CodableEnumeration {
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        do {
            let decoded = try container.decode(RawValue.self)
            self = Self.init(rawValue: decoded) ?? Self.defaultCase
        } catch {
            self = Self.defaultCase
        }
    }
}

让 Sex遵从这个协议:

//MARK:性别
enum Sex:Int,Codable,CodableEnumeration {
    case unknow = 1 //未知
    case man = 2 //男
    case female = 3 //女
    
    static var defaultCase: Sex { //异常处理,如果出现了1、2、3之外的数字,性别默认是unknow
        return .unknow
    }
}

6.3 处理数组


let json = """
 
[
    {
        "name": "lichangan",
        "location": {
            "country": "China"
        },
        "use": "to buy a new collection of clothes to stock her shop before the holidays.",
        "loan_amount": 150
    },
    {
        "name": "Las Margaritas Group",
        "location": {
            "country": "Colombia"
        },
        "use": "to purchase coal in large quantities for resale.",
        "loan_amount": 200
    }
]
 
"""

只需要修改下面这段代码即可。

let loan = try decoder.decode([Loan].self, from: jsonData)

有时您可能想忽略一些键/值对。假设我们像这样更新 json 变量:

let json = """
{
    "paging": {
        "page": 1,
        "total": 6083,
        "page_size": 20,
        "pages": 305
    },
    "loans": [
        {
            "name": "John Davis",
            "location": {
                "country": "Paraguay"
            },
            "use": "to buy a new collection of clothes to stock her shop before the holidays.",
            "loan_amount": 150
        },
        {
            "name": "Las Margaritas Group",
            "location": {
                "country": "Colombia"
            },
            "use": "to purchase coal in large quantities for resale.",
            "loan_amount": 200
        }
    ]
}
"""

该JSON数据带有两个顶层对象:paging 和 loans。
为此,声明另一个名为 LoanDataStore 的结构,该结构也采用 Codable:

struct LoanDataStore: Codable {
    var loans: [Loan]
}

此 LoanDataStore 仅具有与 JSON 数据的 loans 匹配的 loans 属性。
现在,从中修改以下代码行:

let loans = try decoder.decode([Loan].self, from: jsonData)

let loanDataStore = try decoder.decode(LoanDataStore.self, from: jsonData)

解码器将自动解码 loans JSON 对象,并将其存储到 LoanDataStore 的借贷数组中。您可以添加以下代码行来验证数组的内容:

for loan in loanDataStore.loans {
    print(loan)
}

6.4 纯数组中的对象带有唯一Key

如果数据是由多个字典组成的数组,字典里又有一组键值对,这种格式可以看成是前两种的组合:

let res = """
[
    {
        "student": {
            "name": "ZhangSan",
            "age": 17,
            "sex": "male",
            "born_in": "China"
        }
    },
    {
        "student": {
            "name": "LiSi",
            "age": 18,
            "sex": "male",
            "born_in": "Japan"
        }
    },
    {
        "student": {
            "name": "WangWu",
            "age": 16,
            "sex": "male",
            "born_in": "USA"
        }
    }
]
"""

解析这种数据,我们像第二种方式一样,对于外围的数组我们只需要在内层的类型中加上一个中括号就可以了,而里面的类型这里我们需要定义成Dictionary<String, Student>:

struct Student: Codable {
    let name: String
    let age: Int
    let sex: SexType
    let bornIn: String
    
    enum SexType: String, Codable {
        case male
        case female
    }
    
    enum CodingKeys: String, CodingKey {
        case name
        case age
        case sex
        case bornIn = "born_in"
    }
}
let stu = try! decode(of: res, type: [Dictionary<String, Student>].self)
dump(stu)
try! encode(of: stu)

6.5 更一般的复杂情况

接下来我们看一种类型,对于这种类型相对之前更复杂,但处理起来也是很简单,日常开发中也是接触最多这种情况:

let res = """
{
    "info": {
        "grade": "3",
        "classes": "1112"
    },
    "students" : [
        {
            "name": "ZhangSan",
            "age": 17,
            "sex": "male",
            "born_in": "China"
        },
        {
            "name": "LiSi",
            "age": 18,
            "sex": "male",
            "born_in": "Japan"
        },
        {
            "name": "WangWu",
            "age": 16,
            "sex": "male",
            "born_in": "USA"
        }
    ]
}
"""

我们按照老套路一个一个来定制模型其实也是很简单的:

struct Response: Codable {
    let info: Info
    let students: [Student]
    
    struct Info: Codable {
        let grade: String
        let classes: String
    }
    
    struct Student: Codable {
        let name: String
        let age: Int
        let sex: SexType
        let bornIn: String
        
        enum SexType: String, Codable {
            case male
            case female
        }
        
        enum CodingKeys: String, CodingKey {
            case name
            case age
            case sex
            case bornIn = "born_in"
        }
    }
}
let response = try! decode(of: res, type: Response.self)
dump(response)
try! encode(of: response)

6.6 动态键值结构

很多时候由于产品功能的需要,Web服务通常会下发动态结构数据,比如下面这段简化的JSON结构:

{
    "template":"video",
     "videoFeed":{
         "vid":"1234",
        "url":"http://www.baidu.com",
        "coverPic":"http://www.baidu.com/pic.png"
     },
     "picFeed":{
         "content":"今天天气不错哦",
         "pics":{
                "width":100,
                "height":200
          }
      },
      "linkFeed":{
          "title":"四季沐歌",
          "url":"http://www.google.com"
      }
}

其中,template代表模版类型,有三种可能video,pic,link;同一个时刻,Web服务只会下发一种数据。
比如视频模式时:

{
    "template":"video",
     "videoFeed":{
         "vid":"1234",
        "url":"http://www.baidu.com",
        "coverPic":"http://www.baidu.com/pic.png"
     }
}

图文模式时:

{
    "template":"pic",
     "picFeed":{
         "content":"今天天气不错哦",
         "pics":{
                "width":100,
                "height":200
          }
      }
}

如果想要处理好这种动态数据结构,那么就必须要重写init方法和encode方法了。
为了简化问题,这里只实现init方法:

struct Feed:Codable {
    var template:FeedTemplate
    var videoFeed:VideoFeed?
    var picFeed:PicFeed?
    var linkFeed:LinkFeed?
        
    private enum CodingKeys:String,CodingKey{
        case template
        case videoFeed
        case picFeed
        case linkFeed
     }
        
     init(from decoder: Decoder) throws {
         let container = try decoder.container(keyedBy: CodingKeys.self)
         template = try container.decode(FeedTemplate.self, forKey: .template)
         do {
             videoFeed = try container.decodeIfPresent(VideoFeed.self, forKey: .videoFeed)
         } catch {
             videoFeed = nil
         }
         do {
             picFeed = try container.decodeIfPresent(PicFeed.self, forKey: .picFeed)
         } catch {
             picFeed = nil
         }
         do {
             linkFeed = try container.decodeIfPresent(LinkFeed.self, forKey: .linkFeed)
         } catch {
             linkFeed = nil
         }
     }
}
    
struct VideoFeed:Codable {
     var vid:String
     var url:String
     var coverPic:String
}
    
struct PicFeed:Codable {
     var content:String
     var pics:PicFeedImage
}
    
struct PicFeedImage:Codable{
     var width:Int
     var height:Int
}
    
struct LinkFeed:Codable{
     var title:String
     var url:String
}
    
enum FeedTemplate:String,Codable{
     case FEED_VIDEO = "video"
     case FEED_PIC = "pic"
     case FEED_LINK = "link"
}

其中,出现了我们之前没有提到的decodeIfPresent方法。当不确定键值是否会存在时,在设置属性时把这个属性设置为可选,然后使用decodeIfPresent这个方法会查找该键值是否存在,如果存在就decode,如果不存在就会返回nil,这样就可以简单处理动态数据结构造成的问题。

7、空对象或空值

7.1 空对象

在复杂业务场景下,很可能我们需要处理的的数据结构比较复杂,不同的字段key对应相同的数据结构,但是可能有值,也可能只是返回空值,比如有这样两个字段firstFeed和sourceFeed具有相同的JSON结构,但是由于业务需要,在不同场景下firstFeed可能有值(结构与sourceFeed一致),也有可能没有值,返回空对象{},这时应该如何处理呢?

{
    "firstFeed": {},
    "sourceFeed": {
        "feedId": "408255848708594304",
        "title": "“整个宇宙的星星都在俯身望你”    \nphoto by overwater"
     }
}

根据以往的经验,我们尝试使用下面的方式去解析:

class SourceFeed: Codable{
    public var feedId: String
    public var title: String
}

class Feed: Codable {
    public var firstFeed: SourceFeed? 
    public var sourceFeed: SourceFeed
}

var decoder = JSONDecoder()
let feed = try decoder.decode(Feed.self, from: json)

print(feed.firstFeed)
print(feed.sourceFeed.feedId)

如果你运行会发现错误,提示firstFeed没有指定的key存在。

▿ keyNotFound : 2 elements
    - .0 : CodingKeys(stringValue: "feedId", intValue: nil).1 : Context
      ▿ codingPath : 1 element
        - 0 : CodingKeys(stringValue: "firstFeed", intValue: nil)
      - debugDescription : "No value associated with key CodingKeys(stringValue: \"feedId\", intValue: nil) (\"feedId\")."
      - underlyingError : nil

这时我们需要调整SourceFeed的写法:

class SourceFeed: Codable{
    public var feedId: String?
    public var title: String?
}

把SourceFeed的每个属性设置为可选值,这样由于Feed对象中的firstFeed也是可选值,就可以在firstFeed返回空对象{}时,自动解析为nil 。

7.2 空值null

空值null经常会被服务器返回,如果使用Objective-C,null被默认转换为NSNull,如果没有经过检查类型而进行强制类型转换,很容易造成Crash,Swift语言引入Codable后,可以将可能为null的值的类型设置为可选,这样Codable可以自动将null映射为nil,很容易就解决了这个问题。

还以上面的SourceFeed为例:

{
    "firstFeed": null,
    "sourceFeed": {
        "feedId": "408255848708594304",
        "title": "“整个宇宙的星星都在俯身望你”    \nphoto by overwater"
     }
}
class SourceFeed: Codable{
    public var feedId: String
    public var title: String
}

class Feed: Codable {
    public var firstFeed: SourceFeed? 
    public var sourceFeed: SourceFeed
}

var decoder = JSONDecoder()
let feed = try decoder.decode(Feed.self, from: json)

print(feed.firstFeed)
print(feed.sourceFeed.feedId)
//输出结果
//nil
//408255848708594304

如果需要自定义CodingKey,那么null值需要特殊处理

 enum CodingKeys:String,CodingKey {
        case pictures
   }
    
   init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
 
        do { //处理服务器下发的null
            let pic:String = try container.decode(String.self, forKey: .pictures)
        }catch {
            pictures = nil
        }
    }

8、 处理JSON中的日期格式,浮点数,Base64编码,URL

8.1 日期格式

struct Student: Codable {
    let registerTime: Date
    
    enum CodingKeys: String, CodingKey {
        case registerTime = "register_time"
    }
}

如果我们不想时间以浮点数的形式来出现,我们可以对encoder的dateEncodingStrategy属性进行一些设置:

encoder.dateEncodingStrategy = .iso8601
// "register_time" : "2017-11-13T06:48:40Z"
let formatter = DateFormatter()
formatter.dateFormat = "MMM-dd-yyyy HH:mm:ss zzz"
encoder.dateEncodingStrategy = .formatted(formatter)
// "register_time" : "Nov-13-2017 14:55:30 GMT+8"

8.2 浮点数

有时服务器返回一个数据是一些特殊值时,例如返回的学生高度的数值是一个NaN,这时我们对decoder的nonConformingFloatDecodingStrategy属性进行设置:

struct Student: Codable {
    let height: Float
}

let res = """
{
    "height": "NaN"
}
"""
let json = res.data(using: .utf8)!
let decoder = JSONDecoder()
decoder.nonConformingFloatDecodingStrategy = .convertFromString(positiveInfinity: "+∞", negativeInfinity: "-∞", nan: "NaN")
print((try! decoder.decode(Student.self, from: json)).height) //nan

3.3 Base64编码

有时服务器返回一个base64编码的数据时,我们队decoder的dataDecodingStrategy属性进行设置:

struct Student: Codable {
    let blog: Data
}

let res = """
{
    "blog": "aHR0cDovL3d3dy5qaWFuc2h1LmNvbS91c2Vycy8zMjhmNWY5ZDBiNTgvdGltZWxpbmU="
}
"""
let json = res.data(using: .utf8)!
let decoder = JSONDecoder()
decoder.dataDecodingStrategy = .base64
let stu = try! decoder.decode(Student.self, from: json)
print(String(data: stu.blog, encoding: .utf8)!)
// http://www.jianshu.com/users/328f5f9d0b58/timeline

8.4 URL

而对于URL来说,直接映射就可以了

struct Student: Codable {
    let blogUrl: URL
}
let res = """
{
    "blogUrl": "http://www.jianshu.com/users/328f5f9d0b58/timeline"
}
"""
let json = res.data(using: .utf8)!
let decoder = JSONDecoder()
print(try! decoder.decode(Student.self, from: json).blogUrl)
// http://www.jianshu.com/users/328f5f9d0b58/timeline

9、重写系统的方法,实现与系统一样的decode和encode效果

在自定义前,我们先来把这两个方法重写成系统默认的实现来了解一下,对于这两个方法,我们要掌握的是container的用法。

struct Student:Codable {
    var name:String
    var age:Int
    var bornIn:String
    
    init(name:String,age:Int,bornIn:String) {
        self.name = name
        self.age = age
        self.bornIn = bornIn
    }
    
    //重写decoding
    init(from decoder: Decoder) throws {
        //  通过指定映射规则来创建解码容器,通过该容器获取json中的数据,因此是个常量
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let name = try container.decode(String.self, forKey: .name)
        let age = try container.decode(Int.self, forKey: .age)
        let bornIn = try container.decode(String.self, forKey: .bornIn)
        self.init(name: name, age: age, bornIn: bornIn)
    }
    
    
    //重写encoding
    func encode(to encoder: Encoder) throws {
        //通过指定映射规则来创建编码码容器,通过往容器里添加内容最后生成json,因此是个变量
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
        try container.encode(age, forKey: .age)
        try container.encode(bornIn, forKey: .bornIn)
    }
    
    //映射规则,用来指定属性和json中key两者间的映射的规则
    enum CodingKeys:String,CodingKey {
        case name
        case age
        case bornIn = "born_in"
    }
    
}

对于编码和解码的过程,我们都是创建一个容器,该容器有一个keyedBy的参数,用于指定属性和json中key两者间的映射的规则,因此这次我们传CodingKeys的类型过去,说明我们要使用该规则来映射。对于解码的过程,我们使用该容器来进行解码,指定要值的类型和获取哪一个key的值,同样的,编码的过程中,我们使用该容器来指定要编码的值和该值对应json中的key,他们看起来有点像Dictionary的用法。

10、使用struct来遵守CodingKey来指定映射规则

模型中定义的CodingKeys映射规则是用enum来遵守CodingKey协议实现的,其实我们还可以把CodingKeys的类型定义一个struct来实现CodingKey协议:

    // 映射规则,用来指定属性和json中key两者间的映射的规则
//    enum CodingKeys: String, CodingKey {
//        case name
//        case age
//        case bornIn = "born_in"
//    }
    
    // 映射规则,用来指定属性和json中key两者间的映射的规则
    struct CodingKeys: CodingKey {
        var stringValue: String //key
        var intValue: Int? { return nil }
        init?(intValue: Int) { return nil }
        
        // 在decode过程中,这里传入的stringValue就是json中对应的key,然后获取该key的值
        // 在encode过程中,这里传入的stringValue就是生成的json中对应的key,然后设置key的值
        init?(stringValue: String) {
            self.stringValue = stringValue
        }
        // 相当于enum中的case
        static let name = CodingKeys(stringValue: "name")!
        static let age = CodingKeys(stringValue: "age")!
        static let bornIn = CodingKeys(stringValue: "born_in")!
    }

使用结构体来遵守该协议需要实现该协议的内容,这里因为我们的json中的key是String类型,所以用不到intValue,因此返回nil即可。重新运行,结果仍然是正确的。不过需要注意的是,如果 不是 使用enum来遵守CodingKey协议的话,例如用struct,我们 必须 重写Codable协议里的编码和解码方法,否者就会报错:

cannot automatically synthesize 'Decodable' because 'CodingKeys' is not an enum
cannot automatically synthesize 'Encodable' because 'CodingKeys' is not an enum

因此,使用struct来遵守CodingKey,比用enum工程量大。那为什么还要提出这种用法?因为在某些特定的情况下它还是有出场的机会,使用struct来指定映射规则更灵活,到在第三篇中的一个例子就会讲到使用的场景,这里先明白它的工作方式。

11、 自定义Encoding

在自定义encode中,我们需要注意的点是对时间格式处理,Optional值处理以及数组处理。

11.1 时间格式处理

方法一:在encode方法中处理

struct Student: Codable {
    let registerTime: Date
    
    enum CodingKeys: String, CodingKey {
        case registerTime = "register_time"
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        let formatter = DateFormatter()
        formatter.dateFormat = "MMM-dd-yyyy HH:mm:ssZ"
        let stringDate = formatter.string(from: registerTime)
        try container.encode(stringDate, forKey: .registerTime)
    }
}

方法二: 对泛型函数中对JSONEncoder对象的dateEncodingStrategy属性进行设置

encoder.dateEncodingStrategy = .custom { (date, encoder) in
        let formatter = DateFormatter()
        formatter.dateFormat = "MMM-dd-yyyy HH:mm:ssZ"
        let stringDate = formatter.string(from: date)
        var container = encoder.singleValueContainer()
        try container.encode(stringDate)
    }

这里创建的容器是一个singleValueContainer,因为这里不像encode方法中那样需要往容器里一直添加值,所以使用一个单值容器就可以了。

try! encode(of: Student(registerTime: Date()))
//{
//  "register_time" : "Nov-13-2017 20:12:57+0800"
//}

11.2 Optional值处理

如果模型中有属性是可选值,并且为nil,当我进行encode时该值是不会以null的形式写入json中:

struct Student: Codable {
    var scores: [Int]?
}
try! encode(of: Student())
//{
//
//}

为系统对encode的实现其实不是像我们上面所以写的那样用container调用encode方法,而是调用encodeIfPresent这个方法,该方法对nil则不进行encode。我们可以强制将friends写入json中:

struct Student: Codable {
    var scores: [Int]?
    
    enum CodingKeys: String, CodingKey {
        case scores
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(scores, forKey: .scores)
    }
}
try! encode(of: Student())
//{
//    "scores" : null
//}

11.3 数组处理

有时候,我们想对一个数组类型的属性进行处理后再进行encode,或许你会想,使用一个compute property处理就可以了,但是你只是想将处理后的数组进行encode,原来的数组则不需要,于是你自定义encode来实现,然后!你突然就不想多写一个compute property,只想在encode方法里进行处理,于是我们可以使用container的nestedUnkeyedContainer(forKey:)方法创建一个UnkeyedEncdingContainer(顾名思义,数组是没有key的)来对于数组进行处理就可以了。

struct Student: Codable {
    let scores: [Int] = [66, 77, 88]
    
    enum CodingKeys: String, CodingKey {
        case scores
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        // 创建一个对数组处理用的容器 (UnkeyedEncdingContainer)
        var unkeyedContainer = container.nestedUnkeyedContainer(forKey: .scores)
        try scores.forEach {
            try unkeyedContainer.encode("\($0)分")
        }
    }
}
try! encode(of: Student())
//{
//    "scores" : [
//    "66分",
//    "77分",
//    "88分"
//    ]
//}

12、自定义Decoding

对于自定义decode操作上与自定义encode类似,需要说明的点同样也是时间格式处理,数组处理,但Optional值就不用理会了。

12.1 时间格式处理

struct Student: Codable {
    let registerTime: Date
    
    enum CodingKeys: String, CodingKey {
        case registerTime = "register_time"
}

    init(registerTime: Date) {
        self.registerTime = registerTime
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let registerTime = try container.decode(Date.self, forKey: .registerTime)
        self.init(registerTime: registerTime)
    }
}

let res = """
{
    "register_time": "2017-11-13 22:30:15 +0800"
}
"""
let stu = try! decode(of: res, type: Student.self)// error: Expected to decode Double but found a string/data instead.

因为我们这里时间的格式不是一个浮点数,而是有一定格式化的字符串,因此我们要进行对应的格式匹配,操作也是和自定义encode中的类似,修改init(from decoder: Decoder方法:

 init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let dateString = try container.decode(Date.self, forKey: .registerTime)
        let formaater = DateFormatter()
        formaater.dateFormat = "yyyy-MM-dd HH:mm:ss z"
        let registerTime = formaater.date(from: dateString)!
        self.init(registerTime: registerTime)
    }

或者我们可以在JSONDecoder对象对dateDncodingStrategy属性使用custom来修改:

decoder.dateDecodingStrategy = .custom{ (decoder) -> Date in
        let container = try decoder.singleValueContainer()
        let dateString = try container.decode(String.self)
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z"
        return formatter.date(from: dateString)!
    }

12.2 数组处理

当我们获取这样的数据:

let res = """
{
    "gross_score": 120,
    "scores": [
        0.65,
        0.75,
        0.85
    ]
}
"""

gross_score代表该科目的总分数,scores里装的是分数占总分数的比例,我们需要将它们转换成实际的分数再进行初始化。对于数组的处理,我们和自定义encoding时所用的容器都是UnkeyedContainer,通过container的nestedUnkeyedContainer(forKey: )方法创建一个UnkeyedDecodingContainer,然后从这个unkeyedContainer中不断取出值来decode,并指定其类型。

struct Student: Codable {
    let grossScore: Int
    let scores: [Float]
    
    enum CodingKeys: String, CodingKey {
        case grossScore = "gross_score"
        case scores
    }
    
    init(grossScore: Int, scores: [Float]) {
        self.grossScore = grossScore
        self.scores = scores
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let grossScore = try container.decode(Int.self, forKey: .grossScore)
        
        var scores = [Float]()
        // 处理数组时所使用的容器(UnkeyedDecodingContainer)
        var unkeyedContainer = try container.nestedUnkeyedContainer(forKey: .scores)
        // isAtEnd:A Boolean value indicating whether there are no more elements left to be decoded in the container.
        while !unkeyedContainer.isAtEnd {
            let proportion = try unkeyedContainer.decode(Float.self)
            let score = proportion * Float(grossScore)
            scores.append(score)
        }
        self.init(grossScore: grossScore, scores: scores)
    }
}

12.3 扁平化JSON的编码和解码

现在我们已经熟悉了自定义encoding和decoding的过程了,也知道对数组处理要是container创建的nestedUnkeyedContainer(forKey: )创建的unkeyedContainer来处理。现在我们来看一个场景,假设我们有这样一组含嵌套结构的数据:

let res = """
{
    "name": "Jone",
    "age": 17,
    "born_in": "China",
    "meta": {
        "gross_score": 120,
        "scores": [
            0.65,
            0.75,
            0.85
        ]
    }
}
"""

而我们定义的模型的结构却是扁平的:

struct Student {
    let name: String
    let age: Int
    let bornIn: String
    let grossScore: Int
    let scores: [Float]
}

对于这类场景,我们可以使用container的nestedContainer(keyedBy:, forKey: )方法创建的KeyedContainer处理,同样是处理内嵌类型的容器,既然有处理像数组这样unkey的内嵌类型的容器,自然也有处理像字典这样有key的内嵌类型的容器,在encoding中是KeyedEncodingContainer类型,而在decoding中当然是KeyedDecodingContainer类型,因为encoding和decoding中它们是相似的:

struct Student: Codable {
    let name: String
    let age: Int
    let bornIn: String
    let grossScore: Int
    let scores: [Float]
    
    enum CodingKeys: String, CodingKey {
        case name
        case age
        case bornIn = "born_in"
        case meta
    }
    
    // 这里要指定嵌套的数据中的映射规则
    enum MetaCodingKeys: String, CodingKey {
        case grossScore = "gross_score"
        case scores
    }


    init(name: String, age: Int, bornIn: String, grossScore: Int, scores: [Float]) {
        self.name = name
        self.age = age
        self.bornIn = bornIn
        self.grossScore = grossScore
        self.scores = scores
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let name = try container.decode(String.self, forKey: .name)
        let age = try container.decode(Int.self, forKey: .age)
        let bornIn = try container.decode(String.self, forKey: .bornIn)
        
        // 创建一个对字典处理用的容器 (KeyedDecodingContainer),并指定json中key和属性名的规则
        let keyedContainer = try container.nestedContainer(keyedBy: MetaCodingKeys.self, forKey: .meta)
        let grossScore = try keyedContainer.decode(Int.self, forKey: .grossScore)
        var unkeyedContainer = try keyedContainer.nestedUnkeyedContainer(forKey: .scores)
        var scores = [Float]()
        while !unkeyedContainer.isAtEnd {
            let proportion = try unkeyedContainer.decode(Float.self)
            let score = proportion * Float(grossScore)
            scores.append(score)
        }
        self.init(name: name, age: age, bornIn: bornIn, grossScore: grossScore, scores: scores)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
        try container.encode(age, forKey: .age)
        try container.encode(bornIn, forKey: .bornIn)
        
        // 创建一个对字典处理用的容器 (KeyedEncodingContainer),并指定json中key和属性名的规则
        var keyedContainer = container.nestedContainer(keyedBy: MetaCodingKeys.self, forKey: .meta)
        try keyedContainer.encode(grossScore, forKey: .grossScore)
        var unkeyedContainer = keyedContainer.nestedUnkeyedContainer(forKey: .scores)
        try scores.forEach {
            try unkeyedContainer.encode("\($0)分")
        }
    }
}

13、处理带有派生关系的模型

在使用Codable进行json与模型之间转换,对于模型的类型使用struct是没什么问题,而类型是class并且是基类的话,同样也是没问题的,但是模型是派生类的话,则需要额外的处理,例如来看个小场景。

class Ponit2D: Codable {
    var x = 0.0
    var y = 0.0
}

class Ponit3D: Ponit2D {
    var z = 0.0
}

let p1 = Ponit3D()
try! encode(of: p1)
let res = """
{
    "x" : 1,
    "y" : 1,
    "z" : 1
}
"""
let p2 = try! decode(of: res, type: Ponit3D.self)
dump(p2)

接着我们来看看打印结果:

{
  "x" : 0,
  "y" : 0
}
▿ __lldb_expr_221.Ponit3D #0super: __lldb_expr_221.Ponit2D
    - x: 1.0
    - y: 1.0
  - z: 0.0

实际上,默认Codable中的默认encode和decode方法并不能正确处理派生类对象。因此,当我们的模型是派生类时,要自己编写对应的encode和decode的方法。
首先我们先来实现encode:

class Ponit2D: Codable {
    var x = 0.0
    var y = 0.0
    // 标记为private
    private enum CodingKeys: String, CodingKey {
        case x
        case y
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(x, forKey: .x)
        try container.encode(y, forKey: .y)
    }
}

class Ponit3D: Ponit2D {
    var z = 0.0
    // 标记为private
    private enum CodingKeys: String, CodingKey {
        case z
    }
    
    override func encode(to encoder: Encoder) throws {
        //调用父类的encode方法将父类的属性encode
        try super.encode(to: encoder)
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(z, forKey: .z)
    }
}

let p1 = Ponit3D()
try! encode(of: p1)
//{
//  "x" : 0,
//  "y" : 0,
//  "z" : 0
//}

这里需要说明的是,CodingKeys需要用private标记,防止被派生类继承。其次,在encode方法中,我们要调用super.encode,否则父类的属性将没有进行编码,例如本例中若没有调用super.encode,encodePonit3D对象时则会只有z属性被编码,而x和y属性则不会。而调用super.encode时,我们直接把encoder传递给基类调用,因此基类和派生类共享一个container。当然你也可以为了区分他们单独创建一个container传递给父类。

class Ponit3D: Ponit2D {
    var z = 0.0
    
    private enum CodingKeys: String, CodingKey {
        case z
    }
    
    override func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        // 创建一个提供给父类encode的容器来区分父类属性和派生类属性
        try super.encode(to: container.superEncoder())
        try container.encode(z, forKey: .z)
    }
}

let p1 = Ponit3D()
try! encode(of: p1)
//{
//    "super" : {
//        "x" : 0,
//        "y" : 0
//    },
//    "z" : 0
//}

如果你不喜欢默认的super来做父类属性的key,也可以单独命名,container.superEncoder有一个forKey参数,通过CodingKeys的case来命名:

class Ponit3D: Ponit2D {
    var z = 0.0
    
    private enum CodingKeys: String, CodingKey {
        case z
        case point2D //用于父类属性容器的key名
    }
    
    override func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        // 创建一个提供给父类encode的容器来区分父类属性和派生类属性,并将key设为point2D
        try super.encode(to: container.superEncoder(forKey: .Point2D))
        try container.encode(z, forKey: .z)
    }
}

let p1 = Ponit3D()
try! encode(of: p1)
//{
//    "point2D" : {
//        "x" : 0,
//        "y" : 0
//    },
//    "z" : 0
//}

派生类encode的方法已经重写好了,接下来我们还要重写decode方法。其实decode方法和encode方法非常类似,通过init(from decoder: Decoder) throws方法调用super的方法,传递一个共享容器或则一个单独的容器就可以实现了,这里便不再演示了,有需要的可以查看本文的demo。

14、model兼容多个版本的API

假如有一个场景,一个app版本迭代,服务器对新版本的数据格式做了修改,例如有两个版本的时间格式:

// version1
{
    "time": "Nov-14-2017 17:25:55 GMT+8"
}

// version2
{
    "time": "2017-11-14 17:27:35 +0800"
}

我们要根据版本的不同,上传给服务器的时间格式也不同,这里以encode为例,我们在Encoder的protocol中可以找到一个属性:

public protocol Decoder {
    /// The path of coding keys taken to get to this point in encoding.
    public var codingPath: [CodingKey] { get }
}

我们可以使用这个userInfo属性在存储版本的信息,在encode的时候再读取版本信息来进行格式处理。而userInfo中的key是一个CodingUserInfoKey类型,CodingUserInfoKey和Dictionary中key的用法很类似。现在我们就有思路了,首先我们创建一个版本控制器来规定版本的信息:

struct VersionController {
    enum Version {
        case v1
        case v2
    }
    
    let apiVersion: Version
    var formatter: DateFormatter {
        let formatter = DateFormatter()
        switch apiVersion {
        case .v1:
            formatter.dateFormat = "MMM-dd-yyyy HH:mm:ss zzz"
            break
        case .v2:
            formatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z"
            break
        }
        return formatter
    }
    static let infoKey = CodingUserInfoKey(rawValue: "dateFormatter")!
}

接着我们修改调用的encode泛型函数,添加一个VersionController类型的参数用于传递版本信息:

func encode<T>(of model: T, optional: VersionController? = nil) throws where T: Codable {
    let encoder = JSONEncoder()
    encoder.outputFormatting = .prettyPrinted
    if let optional = optional {
        // 通过userInfo存储版本信息
        encoder.userInfo[VersionController.infoKey] = optional
    }
    let encodedData = try encoder.encode(model)
    print(String(data: encodedData, encoding: .utf8)!)
}

然后我们来编写我们的模型:

struct SomeThing: Codable {
    let time: Date
    
    enum CodingKeys: String, CodingKey {
        case time
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        // 通过userInfo读取版本信息
        if let versionC = encoder.userInfo[VersionController.infoKey] as? VersionController {
            let dateString = versionC.formatter.string(from: time)
            try container.encode(dateString, forKey: .time)
        } else {
            fatalError()
        }
    }
}

最后我们来验证我们的代码:

let s = SomeThing(time: Date())

let verC1 = VersionController(apiVersion: .v1)
try! encode(of: s, optional: verC1)
//{
//    "time" : "Nov-14-2017 20:01:55 GMT+8"
//}
let verC2 = VersionController(apiVersion: .v2)
try! encode(of: s, optional: verC2)
//{
//    "time" : "2017-11-14 20:03:47 +0800"
//}

现在我们已经通过Encoder中的userInfo属性来实现版本控制,对于decode只需在init方法对应实现即可。

15、处理key个数不确定的json

有一种总很特殊的情况就是我们得到这样一个json数据:

let res = """
{
    "1" : {
        "name" : "ZhangSan"
    },
    "2" : {
        "name" : "LiSi"
    },
    "3" : {
        "name" : "WangWu"
    }
}
"""

json中key的个数不确定,并且以学生的学号作为key,我们不能按照json的数据创建一个个的模型,对于这种情况我们又该如何处理?
其实大致思路是这样的:我们同样创建一个包含id属性和name属性的Student模型,接着创建一个StudentList模型,StudentList中有一[Student]类型的属性用于存放Student模型。此时,我们知道系统默认Codable中的方法不能满足我们,我们需要自定义,而使用enum的Codingkeys来指定json中的key和属性的映射规则显然也不能满足我们,我们需要一个更灵活的Codingkeys,因此,我们可以使用上篇所提到的用struct类型实现Codingkeys,如果大家忘了话可以先倒回去看一遍其工作方式,这里就不再重复提了。

struct Student: Codable {
    let id: Int
    let name: String
}

struct StudentList: Codable {
    var students: [Student] = []
    
    init(students: Student ... ) {
        self.students = students
    }
    
    struct Codingkeys: CodingKey {
        var intValue: Int? {return nil}
        init?(intValue: Int) {return nil}
        var stringValue: String //json中的key
        // 根据key来创建Codingkeys,来读取key中的值
        init?(stringValue: String) {
            self.stringValue = stringValue
        }
        // 相当于enum中的case
        // 其实就是读取key是name所应对的值
        static let name = Codingkeys(stringValue: "name")!
    }
}

现在我们有一个比较灵活的Codingkeys,我们接下来要做在decode中遍历container中所有key,因为key的类型是Codingkeys类型,所以我们可以通过key的stringValue属性来读取id,然后创建一个内嵌的keyedContainer来读取key对应的字典,然后再读取name的值,这就是大致的思路:

init(from decoder: Decoder) throws {
        // 指定映射规则
        let container = try decoder.container(keyedBy: Codingkeys.self)
        var students: [Student] = []
        for key in container.allKeys { //key的类型就是映射规则的类型(Codingkeys)
            if let id = Int(key.stringValue) { // 首先读取key本身的内容
                // 创建内嵌的keyedContainer读取key对应的字典,映射规则同样是Codingkeys
                let keyedContainer = try container.nestedContainer(keyedBy: Codingkeys.self, forKey: key)
                let name = try keyedContainer.decode(String.self, forKey: .name)
                let stu = Student(id: id, name: name)
                students.append(stu)
            }
        }
        self.students = students
    }

测试一下代码验证时都正确:

let stuList2 = try! decode(of: res, type: StudentList.self)
dump(stuList2)
//▿ __lldb_expr_752.StudentList
//  ▿ students: 3 elements
//    ▿ __lldb_expr_752.Student
//      - id: 2
//      - name: "LiSi"
//    ▿ __lldb_expr_752.Student
//      - id: 1
//      - name: "ZhangSan"
//    ▿ __lldb_expr_752.Student
//      - id: 3
//      - name: "WangWu"

对于encode的方法,其实就是对着decode的反向来进行,我们只需要方向思考一下就很容易知道如何操作了:

func encode(to encoder: Encoder) throws {
        // 指定映射规则
        var container = encoder.container(keyedBy: Codingkeys.self)
        try students.forEach { stu in
            // 用Student的id作为key,然后该key对应的值是一个字典,所以我们创建一个处理字典的子容器
            var keyedContainer = container.nestedContainer(keyedBy: Codingkeys.self, forKey: Codingkeys(stringValue: "\(stu.id)")!)
            try keyedContainer.encode(stu.name, forKey: .name)
        }
    }

测试一下代码验证时都正确:

let stu1 = Student(id: 1, name: "ZhangSan")
let stu2 = Student(id: 2, name: "LiSi")
let stu3 = Student(id: 3, name: "WangWu")
let stuList1 = StudentList(students: stu1, stu2, stu3)
try! encode(of: stuList1)
//{
//    "1" : {
//        "name" : "ZhangSan"
//    },
//    "2" : {
//        "name" : "LiSi"
//    },
//    "3" : {
//        "name" : "WangWu"
//    }
//}

16、Coable中错误的类型(EncodingError & DecodingError)

在编码和解码是会出现的错误类型是DecodingError和EncodingError。我们先来看看DecodingError:

public enum DecodingError : Error {
    // 在出现错误时通过context来获取错误的详细信息
    public struct Context {
        public let codingPath: [CodingKey]
        // 错误信息中的具体错误描述
        public let debugDescription: String
        public let underlyingError: Error?
        public init(codingPath: [CodingKey], debugDescription: String, underlyingError: Error? = default)
    }
    /// 下面是错误的类型
    // JSON值和model类型不匹配
    case typeMismatch(Any.Type, DecodingError.Context)
    // 不存在的值
    case valueNotFound(Any.Type, DecodingError.Context)
    // 不存在的key
    case keyNotFound(CodingKey, DecodingError.Context)
    // 不合法的JSON格式
    case dataCorrupted(DecodingError.Context)
}

相对DecodingError,EncodingError的错误类型只有一个:

public enum EncodingError : Error {
    // 在出现错误时通过context来获取错误的详细信息
    public struct Context {
        public let codingPath: [CodingKey]
        // 错误信息中的具体错误描述
        public let debugDescription: String
        public let underlyingError: Error?
        public init(codingPath: [CodingKey], debugDescription: String, underlyingError: Error? = default)
    }
    // 属性的值与类型不合符
    case invalidValue(Any, EncodingError.Context)
}
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值