Swift中JSON对象化处理

引言

在处理JSON数据时,Swift4中有个强大的协议 Codable,可以用于数据解析(不懂的可以看下Codable的基本功能)。这里,结合实践,总结swift中 JSON 数据的处理,主要包括:

  1. 基本的解析和对象化
  2. 对象化进阶
  3. 其它:错误处理

JSON数据的序列化和对象化

一个简单的 JSON 数据:

{
    "taskName": "Solor",
    "taskNumber": 308001,
    "starDate": "05-30-2019",
    "isAssigned": false
}

可以使用 JSONSerialization 进行序列化:

var jsonData = jsonString.data(using: .utf8)!
var task = try JSONSerialization.jsonObject(with: jsonData, options: .mutableLeaves)
if let taskdic = task as? Dictionary<String, Any> {
    taskdic["taskName"]
}

而在实际开发中,我们往往会根据数据定义一个 model,来更好地进行面向对象的开发:

struct ZZHTask {
    let taskName: String
    let taskNumber: Int
    let startDate: String
    let isAssigned: Bool
}
extension ZZHTask: Decodable {
}

如上,定义一个struct,并实现Decodable协议,接下来就可以解析json数据并生成对象:

let decoder = JSONDecoder()
task = try decoder.decode(ZZHTask.self, from: jsonData)

如果JOSN数据中使用的是下划线分割,那在定义model的时候,可以使用CodingKey做映射:

extension ZZHTask: Decodable {
    enum CodingKeys: String, CodingKey {
        case taskName
        case taskNumber
        case startDate
        case isAssigned
        case taskMission = "task_mission"
    }
}

此时对应的 JSON 需要添加: "task_mission": "this is to do something"
或许你会注意到,model中 startDate 类型是 String,而显然它应该是 Date 对象。由于JSON中日期字符串的格式只包含日月年,这时需要指定一个format并赋值给 decoder:

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

于是,在解析Date类型的时候, decoder会使用上述的 customerFormat 进行解析。

进阶用法

这部分针对的是对象化。JSON 数据需要做序列化还是对象化不是本文的讨论内容,我秉承的简单原则:如果数据需要在多处使用,那么做对象化

嵌套

假如JSON内容变为:

{
    "taskName": "Solor",
    "taskNumber": 100105,
    "startDate": "05-30-2019",
    "isAssigned": false,
    "task_mission": "this is to do something",
    "taskConditions": {
        "load": 5000,
        "temperature": 50,
        "pressure": 1200
    }
}

这时需要创建一个新 model,而 decode 的部分无需修改:

struct ZZHCondition {
    let load: Int
    let temperature: Int
    let pressure: Int
}
extension ZZHCondition: Decodable {

}
// ZZHTask 中添加
let taskCondition: ZZHCondition
// ZZHTask extension 中添加
case taskCondition

扁平化

继续修改json数据:

{
    "taskName": "Solor",
    "taskNumber": 100105,
    "startDate": "05-30-2019",
    "isAssigned": false,
    "task_mission": "this is to do something",
    "taskCondition": {
        "load": 5000,
        "temperature": 50,
        "pressure": 1200
    },
    "assigner": {
        "csr_name": "Wang Gang"
    }
}

这里添加了 assigner, 其值是一个键值对。我们完全可以照此新添加model类 Assigner,但从业务出发,我们可以理解assigner的值直接是 Wang Gang会更加恰当。因此,我们修改 ZZHTask :

struct ZZHTask {
    let taskName: String
    let taskNumber: Int
    let startDate: Date
    let isAssigned: Bool
    let taskMission: String
    let taskCondition: ZZHCondition
    let assigner: String
}

因为 assigner 对应的并不是一个 String,decode的时候需要做一些修改:将 csr_name 对应的值赋给 assigner。

extension ZZHTask: Decodable {
    enum CodingKeys: String, CodingKey {
        case taskName
        case taskNumber
        case startDate
        case isAssigned
        case taskMission = "task_mission"
        case taskCondition
        case assigner

        enum AssignerKey: String, CodingKey {
            case csrName = "csr_name"
        }
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        taskName = try container.decode(String.self, forKey: .taskName)
        taskNumber = try container.decode(Int.self, forKey: .taskNumber)
        startDate = try container.decode(Date.self, forKey: .startDate)
        isAssigned = try container.decode(Bool.self, forKey: .isAssigned)
        taskMission = try container.decode(String.self, forKey: .taskMission)
        taskCondition = try container.decode(ZZHCondition.self, forKey: .taskCondition)

        let assignContainer = try container.nestedContainer(keyedBy: CodingKeys.AssignerKey.self, forKey: .assigner)
        assigner = try assignContainer.decode(String.self, forKey: .csrName)
    }
}

首先,我们在 ZZHTask 中添加了属性 assigner;对应在CodingKeys中添加相同的case,以满足 Decodable 协议。你也可以看到,extension中添加了 AssignerKey,这是用来decode嵌套的assign对象。因为此时model ZZHTask的属性和JSON数据不是一一对应的,所以需要手动实现 init(from decoder: Decoder)。其中,和assigner相关的部分就是:

let assignContainer = try container.nestedContainer(keyedBy: CodingKeys.AssignerKey.self, forKey: .assigner)
assigner = try assignContainer.decode(String.self, forKey: .csrName)

先获取该嵌套对象对应的容器,再通过key去解析容器中对应的值,并赋给 assigner 属性。

数组

关于Array,先看简单的情形,假如在JSON中添加键值:

"parts":[
    {"number": "WD062781", "name": "fep"},
    {"number": "WF065212", "name": "ltp"}
],

这种情形,可以创建一个model,如:

struct ZZHPart {
    let partNumber: String
    let partName: String
}
extension ZZHPart: Decodable {
    enum CodingKeys: String, CodingKey {
        case partNumber = "number"
        case partName = "name"
    }
}

然后在ZZHTask中添加属性: let parts: [ZZHPart],并更新 extension:

case parts
parts = try container.decode([ZZHPart].self, forKey: .parts)

接下来看下稍微复杂点的情形,现在我们的JSON中添加了新的内容:

"workers": {
    "site": [
        {"worker": { "id": "W60001", "name": "Li"}},
        {"worker": { "id": "W60023", "name": "Wan"}}
    ]
},
"sales":[
    {"worker":{"id": "S80010","name": "Cang"}}
]

如果把site节点去掉,workerssales两个节点都是包含worker的数组,并且worker节点也可以扁平化处理(salesparts的不同就在于扁平化)。首先还是先创建一个model:

struct ZZHWorker {
    let workerID: String
    let workerName: String
}

添加model的decodable extension:

extension ZZHWorker: Decodable {
    enum CodingKeys: String, CodingKey {
        case workerID = "id"
        case workerName = "name"
    }

    enum WorkerKey: CodingKey { case worker }

    init(from decoder: Decoder) throws {
        let rootKeys        = try decoder.container(keyedBy: WorkerKey.self)
        let workerContainer  = try rootKeys.nestedContainer(keyedBy: CodingKeys.self, forKey: .worker)
        workerID = try workerContainer.decode(String.self, forKey: .workerID )
        workerName = try workerContainer.decode(String.self, forKey: .workerName)
    }
}

worker节点扁平化的工作放在了model中进行,因为这样处理workers节点的时候,可以复用。接下来,需要在 ZZHTask 添加相应的属性,并且在 extension 中更新:

//ZZHTask
let sales:[ZZHWorker]
//ZZHTask extention
case sales
//ZZHTask extention decoder方法中
var saleContainer = try container.nestedUnkeyedContainer(forKey: .sales)
var saleTmp: [ZZHWorker] = []
while !saleContainer.isAtEnd {
    let work = try saleContainer.decode(ZZHWorker.self)
    saleTmp += [work]
}
sales = saleTmp

和之前不太一样的是,数组需要使用 nestedUnkeyedContainer 来处理。worker的节点处理,是类似的流程,你可以自己实践下,或者查看示例代码

其它

错误处理

对于对象化和序列化,都需要通过 do-catch 处理异常。对于对象化的处理方式,注意属性要设置成optional(上文例子不适用于生产环境)。
对于序列化的处理方式,在取键值的时候,或者使用if或者optional。如果JSON有很多层嵌套的话,那会让人很崩溃。这时,可以使用SwiftyJson,极大地提高代码简洁度。

model的创建

也许你会问对象化处理过程中,需要创建的model太多。不用担心,如ObjC时代,亦有很多的工具来生产Swift中使用的model——struct或class。这里,推荐 JSONExport