Swift中的JSON转换

本文的主要内容来自于https://benscheirman.com/2017/06/swift-json.html,并在此基础上进行了翻译和精简。更多细节可以直接阅读原文。

本文中的Swift版本为5.8

快速开始

有些朋友不喜欢长篇大论的理论和原理讲解,想快速掌握大概的使用方法,所以我在这里举了两个例子,以满足这方面的需求。

/*
 编码JSON字符串
*/

struct Person: Codable {
    var name: String
    var age: Int32
    var height: Double
    var email: String
}

let person = Person(name: "张三", age: 23, height: 1.78,email: "12345@qq.com")
// 创建json编码器
let encoder = JSONEncoder()
// 友好输出,即包含换行和缩进
encoder.outputFormatting = .prettyPrinted

do {
      // 编码
    let jsonData = try encoder.encode(person)
    let jsonString = String(data: jsonData, encoding: .utf8)
    if jsonString != nil {
        print(jsonString!)
    }
} catch {
    print(error.localizedDescription)
}
/*
 解码JSON字符串
*/

struct Person: Codable {
    var name: String
    var age: Int32
    var height: Double
    var email: String
}
let jsonString = """
{"age":23,"email":"12345@qq.com","name":"张三","height":1.78}
"""

let decoder = JSONDecoder()
// 创建json解码器
let jsonData = jsonString.data(using: .utf8)

do {
      // 解码
    let person = try decoder.decode(Person.self, from: jsonData!)
    print(person)
} catch {
    print(error.localizedDescription)
}

如果上面的例子不能解决你的问题,那么我建议全面系统地学习json字符串的转换,正所谓磨刀不负砍柴工。

简单例子

这次在结构体中加入枚举类型,然后将其编码成json字符串。

enum Hobby: String, Codable {
    case painting
    case reading
    case swimming
}

// 如果支持编码和解码,则必须遵循Codable协议
struct Student: Codable {
    var name: String
    var email: String
    
    var hobby: Hobby
}

let stu1 = Student(name: "张三", email: "zhangsan@qq.com", hobby: .painting)
let encoder = JSONEncoder()

do {
    // 因为编解码可能会失败,所以必须要使用try,并处理发生失败的情况
    let dataString = try encoder.encode(stu1)
    let jsonString = String(data: dataString, encoding: .utf8)
    print(jsonString!)
} catch {
    print(error.localizedDescription)
}

上面是将struct编码成json字符串的过程,解码json字符串到struct的流程与上面类似,可尝试自己编写。

自定义字段名

有时候我们并不想把变量名称或常量名称当作json字符串中的字段名,那么可以重新定义CodingKeys枚举,达到自定义json字段名的效果。例子如下。

struct Student: Codable {
    var name: String
    var email: String
    
   // 需要遵循CodingKey协议
    enum CodingKeys: String, CodingKey {
        case name = "Name"
        case email = "Email"
    }
}

let stu = Student(name: "张三", email: "zhangsan@qq.com")
let encoder = JSONEncoder()

do {
    let jsonData = try encoder.encode(stu)
    let jsonString = String(data: jsonData, encoding: .utf8)
    print(jsonString!)
} catch {
    print(error.localizedDescription)
}

处理日期

JSON中并没有对应的日期类型,如果直接编码的话,会输出一串数字,而不是相应的日期格式,所以在编码之前,可以设置编码器中的日期格式。

struct Foo: Codable {
    var date: Date
}
let foo = Foo(date: Date.now)

let encoder = JSONEncoder()
// 设置日期格式
encoder.dateEncodingStrategy = .iso8601

let jsonData = try! encoder.encode(foo)
let jsonString = String(data: jsonData, encoding: .utf8)
print(jsonString!)

当然,如果上面不满足需求,那么可以进一步自定义。

let df = DateFormatter()
df.dateFormat = "yyyy-MM-dd HH:mm:ss"
encoder.dateEncodingStrategy = .formatted(df)

如果还是不能满足需求,那么还可以使用.custom,具体使用方法请查阅相关文档。

处理URL

假设有如下json字符串。

{
    "title": "百度一下",
    "url": "http://baidu.com"
}

那么对应的struct为如下。

struct Webpage: Codable {
    var title: String
    var url: URL
}

解码过程也是相当的简单,如下所示。

let jsonString = """
{
    "title": "百度一下",
    "url": "http://baidu.com"
}
"""
let jsonData = jsonString.data(using: .utf8)
let decoder = JSONDecoder()
var page = try! decoder.decode(Webpage.self, from: jsonData!)

处理顶层数组

假设有如下json字符串。

[{"name":"张三"},{"name":"李四"},{"name":"王五"}]

解码过程如下。

struct Article: Codable {
    var title: String
}

let articlesString = """
[{"title": "标题1"}, {"title":"标题2"}, {"title":"标题3"}]
"""

let data = articlesString.data(using: .utf8)

let decoder = JSONDecoder()
// 注意这里的类型写法,数组中的元素需要遵循Codable协议
var articles = try! decoder.decode([Article].self, from: data!)

for article in articles {
    print(article.title)
}

更复杂的结构

有些复杂的json字符串可以由简单的结构组成,所以在定义这些json结构时,可以使用嵌套完成复杂json的结构体的定义。如下面的例子。

{
    "meta": {
        "page": 1,
        "total_pages": 4,
        "per_page": 10,
        "total_records": 38
    },
    "breweries": [
        {
            "id": 1234,
            "name": "Saint Arnold"
        },
        {
            "id": 52892,
            "name": "Buffalo Bayou"
        }
    ]
}
struct PagedBreweries : Codable {
    struct Meta : Codable {
        let page: Int
        let totalPages: Int
        let perPage: Int
        let totalRecords: Int
        enum CodingKeys : String, CodingKey {
            case page
            case totalPages = "total_pages"
            case perPage = "per_page"
            case totalRecords = "total_records"
        }
    }

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

    let meta: Meta
    let breweries: [Brewery]
}

自定义编解码

上面例子中的编解码过程都不需要我们干预,非常方便我们使用,但如果想更精准的控制编解码过程,那么就需要自定义编解码过程。

编码

这里以车为例子,里面包含一些基本信息,有些信息没有实际意义,仅仅为了演示需要。

enum CarColor: String, Codable {
    case white
    case black
    case red
    case blue
}

struct Car: Codable {
    var owner: String
    var manufacturer: String
    var price: String
    var color: CarColor
    // 无任何意义,仅用于演示
    var sizes: [Double]
}

然后对车进行扩展,实现encode方法。

extension Car {
    
    enum CodingKeys: String, CodingKey {
        case owner
        case manufacturer
        case price
        case color
        case sizes
    }
    
    func encode(to encoder: Encoder) throws {
        // 获取container,具体解释见后面
        var container = encoder.container(keyedBy: CodingKeys.self)
        do {
            // 加入编码数据,第一项为数据的值,第二项为字段的值
            try container.encode(owner, forKey: .owner)
            try container.encode(manufacturer, forKey: .manufacturer)
            try container.encode("\(price)¥", forKey: .price)
            try container.encode(color, forKey: .color)
            // 获取第二种类型的container,准备对数组进行自定义编码过程
            var sizesContainer = container.nestedUnkeyedContainer(forKey: .sizes)
            for size in sizes {
                // 自定义编码数组过程,将每个数组元素翻倍
                try sizesContainer.encode(size * size)
            }
        } catch {
            print(error.localizedDescription)
        }
    }
}

let car = Car(owner: "张三", manufacturer: "比亚迪", price: "336541.23", color: .red, sizes: [1.0, 2.0, 3.0])

let encoder = JSONEncoder()
let jsonData = try! encoder.encode(car)
let jsonString = String(data: jsonData, encoding: .utf8)
print(jsonString!)

上面代码中出现了新的事物,即container。什么是container?原文中并没有进行解释,可以简单理解为管理编码数据的对象。

container有三种类型。

  • 有对应键值的container,本质上是字典,最常用的类型。
  • 无对应键值的container,如数组。
  • 单一值container,不带任何类型的元素的原始值。

在上面代码中,注意container必须是可变属性,需要使用var声明。

解码

解码是编码的逆操作,大部分操作相似,下面例子是对车信息的json字符串进行解码。

let jsonString = """
{"sizes":[1,2,3],"manufacturer":"比亚迪","owner":"张三","price":"336541.23¥","color":"red"}
"""

// 扩展car,实现解码操作
extension Car {
    init(from decoder: Decoder) throws {
        do {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            let owner = try container.decode(String.self, forKey: .owner)
            let manufacturer = try container.decode(String.self, forKey: .manufacturer)
            
            let suffixPrice = try container.decode(String.self, forKey: .price)
            // 去除价格后面的人民币标识
            let price = String(suffixPrice[..<(suffixPrice.firstIndex(of: "¥") ?? suffixPrice.endIndex)])
            
            let color = try container.decode(CarColor.self, forKey: .color)
            
            // 准备解码数组
            var tempSizes: [Double] = []
            // 自定义解码数组数据
            var sizesArray = try container.nestedUnkeyedContainer(forKey: .sizes)
            while (!sizesArray.isAtEnd) {
                let size = try sizesArray.decode(Double.self)
                tempSizes.append(size)
            }
            self.init(owner: owner, manufacturer: manufacturer, price: price, color: color, sizes: tempSizes)
        } catch {
            print(error.localizedDescription)
            self.init(owner: "", manufacturer: "", price: "", color: .black, sizes: [])
        }
    }
}

let decoder = JSONDecoder()
let jsonData = jsonString.data(using: .utf8)
let car = try! decoder.decode(Car.self, from: jsonData!)
print(car)

处理继承关系

class Person : Codable {
    var name: String?
}

class Employee : Person {
    var employeeID: String?
}

如果对Employee编码的话,会出现什么情况?答案是只会编码Person中的属性。为此,我们需要在各自的类中自定义编码过程,并实现相关调用。

class Person : Codable {
    var name: String?

    private enum CodingKeys : String, CodingKey {
        case name
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
    }
}

class Employee : Person {
    var employeeID: String?

    private enum CodingKeys : String, CodingKey {
        case employeeID = "emp_id"
    }

    override func encode(to encoder: Encoder) throws {
        try super.encode(to: encoder) // 试试不加上这个语句会出现什么情况
        var container = encoder.container(keyedBy: CodingKeys.self)
        // try super.encode(to: container.superEncoder())
        try container.encode(employeeID, forKey: .employeeID)
    }
}

let employee = Employee()
employee.name = "张三"
employee.employeeID = "000001"

let encoder = JSONEncoder()
let jsonData = try! encoder.encode(employee)
let jsonString = String(data: jsonData, encoding: .utf8)
print(jsonString!)

上面编码后的结果如下。

{
    "name": "张三",
    "emp_id": "000001"
}

虽然属性都编码成功了,但是结果是扁平的,我们想改为像继承一样具有层级的json字符串,那么将try super.encode(to: encoder)改为try super.encode(to: container.superEncoder())并放在定义container的后面就可以了,编码结果如下。

{
    "super": {
        "name": "张三"
    },
    "emp_id": "000001"
}

上面结果虽然是我们想要的,但是还差点意思,主要原因在于super不是我们想要的名字,我们想要的是person。

class Employee : Person {
    var employeeID: String?

    private enum CodingKeys : String, CodingKey {
        case employeeID = "emp_id"
        // 增加person
        case person
    }

    override func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        // 指定键值
        try super.encode(to: container.superEncoder(forKey: .person))
        try container.encode(employeeID, forKey: .employeeID)
    }
}

总结

本文只介绍了我经常遇到的几种情况下的使用方法,更多使用技巧和方法请阅读原文和官方文档,

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

推荐阅读更多精彩内容