使用Codable解析JSON

2个月前 176次点击 来自 Swift

笔者最近接触最多的场景就是JSON解析,虽然使用开源包大大简化了序列化和反序列化的操作,通过查阅资料还是了解到Codable,Swift4.0标准库中正式引入了Codable接口:

/// A type that can convert itself into and out of an external representation.
///
/// `Codable` is a type alias for the `Encodable` and `Decodable` protocols.
/// When you use `Codable` as a type or a generic constraint, it matches
/// any type that conforms to both protocols.
public typealias Codable = Decodable & Encodable

可以看到它实际上是 Decodable & Encodable 的复合接口,用作数据的解析和编码,先使用一个实例来演示下Codable的使用:

struct Person: Codable {
    var name: String
    var gender: String
    var age: Int
}
let person = Person(name: "devler", gender: "male", age: 18)

//对person实例进行Json编码
let encoder = JSONEncoder()
let data = try! encoder.encode(person)
let personEncodedString = String(data: data, encoding: .utf8)!
print(personEncodedString)

//对Json数据进行解码转换成person实例
let decoder = JSONDecoder()
let personDecoded = try! decoder.decode(Person.self, from: data)
print(personDecoded)

输出为:

{"name":"devler","age":18,"gender":"male"}
Person(name: "devler", gender: "male", age: 18)

1.过滤编码属性

在Json的序列化中可以过滤掉部分字段,只需要声明一个 CodingKeys 枚举属性:

struct Person: Codable {
    var name: String
    var gender: String
    //age属性没有在 CodingKeys 中声明,不会被编码
    var age: Int

    enum CodingKeys: String, CodingKey {
        case name
        case gender
    }
}

输出结果:

{"name":"devler","gender":"male"}

2.键值修改

例如,在不同程序员开发的接口下,有些接口表示性别使用的是gender,而有些接口使用的却是sex,为了适应不同的接口,那么你可以CodingKeys中改变编码属性的名称:

struct Person: Codable {
    var name: String
    var gender: String
    //age属性没有在 CodingKeys 中声明,不会被编码
    var age: Int

    enum CodingKeys: String, CodingKey {
        case name
        case gender = "sex"
    }
}

输出结果:

{"name":"devler","sex":"male"}

3.键值策略

在日常使用,我们总会碰到不同的命名规则,例如下划线命名(person_name)和驼峰命名(personName)以及首字母大写的帕斯卡命名(PersonName),假设你和后端并没有统一命名风格,那么自定义键值或许还能弥补一下。

struct Person: Codable {
    var firstName: String
    var secondName: String
}

let person = Person(firstName: "dev", secondName: "ler")

序列化后的json字符串为

{"firstName":"dev","secondName":"ler"}

假设需要将驼峰命名(personName)转换为下划线命名(person_name):

{"second_name":"ler","first_name":"dev"}

第一种方式是直接在CodingKeys直接修改:

struct Person: Codable {
    var firstName: String
    var secondName: String
    
    enum CodingKeys: String, CodingKey {
        case firstName = "first_name"
        case secondName = "second_name"
    }
}

第二种方式是使用更通用的键值转换策略来解决JSON解码和编码的键策略问题。

    /// The strategy to use for automatically changing the value of keys before encoding.
    public enum KeyEncodingStrategy {
    ...
    /// Provide a custom conversion to the key in the encoded JSON from the keys specified by the encoded types.
    case custom(([CodingKey]) -> CodingKey)
    }
  /// The strategy to use for automatically changing the value of keys before decoding.
    public enum KeyDecodingStrategy {
    ...
    /// Provide a custom conversion from the key in the encoded JSON to the keys specified by the decoded types.
    case custom(([CodingKey]) -> CodingKey)
    }

我们通过扩展JSONEncoder.KeyEncodingStrategy实现编码中的键值转换,通过扩展JSONEncoder.KeyDecodingStrategy来实现解码的键值转换。

在开始编写转换代码之前先介绍一个协议CodingKey(A type that can be used as a key for encoding and decoding.),它是被当做键 (Key) 用于编码和解码的类型。定义了以下四个方法:

protocol CodingKey : CustomDebugStringConvertible, CustomStringConvertible {
    //表示Key为String
  var stringValue: String { get }
  init?(stringValue: String)
    //表示Key为Int
  var intValue: Int? { get }
  init?(intValue: Int)
}

现在我们通过扩展JSONEncoder.KeyEncodingStrategy,来实现custom(([CodingKey]) -> CodingKey),最后JSONEncoder.keyEncodingStrategy就会调用**.custom**键编码策略。

//扩展String实现驼峰和下划线命名规则转换
extension String {
    var camelName: String {
        var result = ""
        var flag = false
        forEach { c in
            let s = String(c)
            if s == "_" {
                flag = true
            } else {
                if flag {
                    result += s.uppercased()
                    flag = false
                } else {
                    result += s
                }
            }
        }
        return result
    }

    var underscore_name: String {
        var result = ""
        forEach { c in
            let num = c.unicodeScalars.map { $0.value }.last!
            let s = String(c)
            if num > 64 && num < 91 {
                result += "_"
                result += s.lowercased()
            } else {
                result += s
            }
        }
        return result
    }
}

struct SimpleCodingKey: CodingKey {
    var stringValue: String
    var intValue: Int?

    init(stringValue: String) {
        self.stringValue = stringValue
    }

    init(intValue: Int) {
        stringValue = "\(intValue)"
        self.intValue = intValue
    }
}

//自定义键值转换策略 - 转为下划线命名规则
extension JSONEncoder.KeyEncodingStrategy {
    static var convertToUnderlineCase: JSONEncoder.KeyEncodingStrategy {
        return .custom { codingKeys in
            let str = codingKeys.last!.stringValue
            return SimpleCodingKey(stringValue: str.underscore_name)
        }
    }
}

struct Person: Codable {
    var firstName: String
    var secondName: String
}

let person = Person(firstName: "dev", secondName: "ler")

// 对person实例进行Json编码
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToUnderlineCase

let data = try! encoder.encode(person)
let personEncodedString = String(data: data, encoding: .utf8)!
print(personEncodedString)

4.结构自定义

例如:

struct Person: Codable {
    var gender: String
    var age: Int
    var firstName: String
    var secondName: String
}

经过序列号后,我们希望得到的层级的JSON数据:

{
    "gender" : "male",
    "age":18,
    "name": {
        "firstName" : "dev"
        "secondName": "ler"
    }
}

此时需要使用NameKeys来进行结构重塑,并手动的确立namefirstName,secondName之间的对应关系,我们通过重载Codable的方法来进行数据的编码和解码:

struct Person: Codable {
    var gender: String
    var age: Int
    var firstName: String
    var secondName: String

    enum CodingKeys: String, CodingKey {
        case gender
        case age
        case name
    }

    enum NameKeys: String, CodingKey {
        case firstName
        case secondName
    }
}

extension Person {
    //解析
    init(from decoder: Decoder) throws {
        let vals = try decoder.container(keyedBy: CodingKeys.self)
        gender = try vals.decode(String.self, forKey: CodingKeys.gender)
        age = try vals.decode(Int.self, forKey: CodingKeys.age)

        let name = try vals.nestedContainer(keyedBy: NameKeys.self, forKey: .name)
        firstName = try name.decode(String.self, forKey: .firstName)
        secondName = try name.decode(String.self, forKey: .secondName)
    }

    //编码
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(gender, forKey: .gender)
        try container.encode(age, forKey: .age)

        var name = container.nestedContainer(keyedBy: NameKeys.self, forKey: .name)
        try name.encode(firstName, forKey: .firstName)
        try name.encode(secondName, forKey: .secondName)
    }
}

let person = Person(gender: "male", age: 18, firstName: "dev", secondName: "ler")

let encoder = JSONEncoder()

let resultData = try! encoder.encode(person)
let personEncodedString = String(data: resultData, encoding: .utf8)!
print(personEncodedString)

总结

Codable简洁又牛逼

Card image cap
开发者雷

尘世间一个小小的开发者,每天增加一些无聊的知识

本站文章全部采用 CC BY 4.0 协议,欢迎转载

技术文档 >> 系列应用 >>
热推应用
Let'sLearnSwift
学习Swift的入门教程
PyPie
Python is as good as Pie
标签