Swift - 使用原生库进行 JSON 解析和生成

快速入门

JSON(JavaScript Object Notation)是应用在 JavaScript 语言上的数据格式,常用于网络数据交换和存储。Apple 在Foundation模块中集成了 JSON 格式数据的解析与生成方法,使得 JSON 数据可以快速简单地导入 Swift 代码和 Core Data 中。

本文将以一段 JSON 数据作为示例,着重讲述如何使用基类来处理 JSON 数据的导入。

JSON 数据分析

假设服务器返回了关于 Swift 商店的数据,包含了商店名称、商店地址、开业时间以及其分店信息:

{
    "shopName": "Swift Shop",
    "address": "2022 Apple Avenue",
    "openYear": 2019,
    "branches": [
        {
            "shopName": "Swift UI Experience",
            "address": "2019 Apple Avenue",
            "branch_manager": "Fiona"
        },
        {
            "shopName": "Codable Protocol",
            "address": "2014 iOS Street",
            "branch_manager": "Steven",
            "closed": true
        }
    ]
}

可以看到这个 JSON 数据除了branch_manager键,其他键命名均为小驼峰规范。第一层是由四个键值对组成的。在branches下,第二层是一个数组并且数据由三个或四个键值对组成,键closed是可选的,默认为false。值得注意的是,第一层和第二层的结构中都有键shopNameaddress

简单使用

如果不需要做任何优化或者没有进一步模型需求,那么有一个网站可以满足 JSON 转 Swift 对象的要求:QuickType。将该 JSON 数据复制到网站中即可生成对应的 Swift 模型代码和解析代码,还有其他高级选项可以进一步定制代码:

QuickType 网站

生成的代码如下:

// This file was generated from JSON Schema using quicktype, do not modify it directly.
// To parse the JSON, add this file to your project and do:
//
//   let shopData = try? newJSONDecoder().decode(Shop.self, from: jsonData)

import Foundation

// MARK: - Shop
class Shop: Codable {
    var shopName, address: String
    let openYear: Int
    let branches: [Branch]

    init(shopName: String, address: String, openYear: Int, branches: [Branch]) {
        self.shopName = shopName
        self.address = address
        self.openYear = openYear
        self.branches = branches
    }
}

// MARK: - Branch
class Branch: Codable {
    let shopName, address, branchManager: String
    let closed: Bool?

    enum CodingKeys: String, CodingKey {
        case shopName, address
        case branchManager = "branch_manager"
        case closed
    }

    init(shopName: String, address: String, branchManager: String, closed: Bool?) {
        self.shopName = shopName
        self.address = address
        self.branchManager = branchManager
        self.closed = closed
    }
}

可以看到Branch类中比Shop类中多了一个enum CodingKeys...结构。这是因为 Swift 中,默认生成或解析数据的变量命名为小驼峰方式,而Branch中有一个branch_manager键不满足要求,所以需要用CodingKey协议下的枚举对这个键名进行映射,并且其他键名也需要在枚举中列出。

QuickType提供的功能已经可以满足大部分的需求了。如果不同层级下有许多键是重复的,这么做会使用大量的代码,既不美观又不够“聪明”。这种情况下,使用子类可以大大提高数据模型的拓展性,使用泛型处理数据会使代码变得更加轻巧。

进阶模型

根据上文的分析,可以先创建一个 BasicShop 类作为基类:

class BasicShop: Codable {
    var shopName: String = ""
    var address: String = ""

    init() {}
}

与上文的初始化方法不同,基类中通过对变量赋初值来简化init()函数。这一步对于子类的初始化是十分重要的。

根据上述定义的基类,先创建第一层级的模型Shop

class Shop: BasicShop {
    var openYear: Int = 0
    var branches: [Branch] = []
    
    private enum CodingKeys: String, CodingKey {
        case openYear
        case branches
    }

    override init() {
        super.init()
    }
    
    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        openYear = try container.decode(Int.self, forKey: .openYear)
        branches = try container.decode([Branch].self, forKey: .branches)
        try super.init(from: decoder)
    }
    
    override public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(openYear, forKey: .openYear)
        try container.encode(branches, forKey: .branches)
        try super.encode(to: encoder)
    }
}

同样的,子类变量需要赋初值,并重写了init()方法;在 Decoder 和 Encoder 的初始化方法中,还需要手动编码对应键值,并调用基类中Codable协议对应的初始化方法。这里注意到有一处与网站生成的代码不同,子类中CodingKeys枚举是必需的,因为在重写 Decoder 和 Encoder 时需要用到对应的映射。为了避免不必要的冲突,CodingKeys枚举被设为private私有的。

class Branch: BasicShop {
    var branchManager: String
    var closed: Bool?

    private enum CodingKeys: String, CodingKey {
        case branchManager = "branch_manager"
        case closed
    }

    override init() {
        super.init()
    }
    
    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        branchManager = try container.decode(String.self, forKey: .branchManager)
        closed = try container.decodeIfPresent(Bool.self, forKey: .closed)
        try super.init(from: decoder)
    }
    
    override public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(branchManager, forKey: .branchManager)
        try container.encode(closed, forKey: .closed)
        try super.encode(to: encoder)
    }
}

值得注意的是,在解析closed键时,使用的是container.decodeIfPresent方法而不是container.decode方法,后者在无法找到键值时会抛出错误,无法正常进行解析

解析和生成 JSON 数据

解析 JSON 数据:

let jsonData = jsonString.data(using: .utf8)
let shopData = try? JSONDecoder().decode(Shop.self, from: jsonData!)

生成 JSON 数据:

let jsonData = try JSONEncoder().encode(shopData)
let jsonString = String(data: jsonData, encoding: .utf8)

参考

QuickType
Pietar-Jan Nefkens

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值