Swift4中Codable的使用(三)

本篇是Swift4中Codable的使用系列第三篇,继续上一篇我们学习了如何自定义encode和decode,以及container的使用。本篇我们继续来了解更多Codable的知识。

处理带有派生关系的模型

在使用Codable进行json与模型之间转换,对于模型的类型使用struct是没什么问题,而类型是class并且是基类的话,同样也是没问题的,但是模型是派生类的话,则需要额外的处理,例如来看个小场景(这里的encode和decode方法均采用上一篇的泛型函数)

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 #0
  ▿ super: __lldb_expr_221.Ponit2D
    - x: 1.0
    - y: 1.0
  - z: 0.0

咦?z去哪了???
实际上,默认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。


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方法对应实现即可。


处理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"
//    }
//}


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

在本系列的最后,我们来了解一下在Coable中会发生哪些错误。在编码和解码是会出现的错误类型是DecodingErrorEncodingError。我们先来看看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)
}

相对DecodingErrorEncodingError的错误类型只有一个:

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)
}

至此,本系列的教学就到此为止了,掌握了Codable的使用会为我们带来许多的便利,可以解决大多数情况的json数据。

本文Demo

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

推荐阅读更多精彩内容