foreword
Codable was introduced with Swift 4.0 and aims to replace the existing NSCoding protocol, supports structures, enumerations and classes, and can convert weak data types such as JSON into strong data types used in code.
Codable is a protocol that takes both Decodable and Encodable into account. If you define a data type that follows the Codable protocol, it actually follows Decodable and Encodable:
typealias Codable = Decodable & Encodable
复制代码
To put it bluntly, it is a set of conversion protocols, which can convert data and data types in Swift according to a certain mapping relationship, such as converting JSON to your custom data type in Swift.
HandyJSON is no longer maintained after the launch of Codable, and our project relies on HandyJSON to handle JSON serialization and deserialization, so we also need to gradually migrate to Codable to avoid some errors.
skills
1. Basic use
struct Person: Codable {
let name: String
let age: Int
}
// 解码
let json = #" {"name":"Tom", "age": 2} "#
let person = try JSONDecoder().decode(Person.self, from: json.data(using: .utf8)!)
print(person) //Person(name: "Tom", age: 2
// 编码
let data0 = try? JSONEncoder().encode(person)
let dataObject = try? JSONSerialization.jsonObject(with: data0!, options: [])
print(dataObject ?? "nil") //{ age = 2; name = Tom; }
let data1 = try? JSONSerialization.data(withJSONObject: ["name": person.name, "age": person.age], options: [])
print(String(data: data1!, encoding: .utf8)!) //{"name":"Tom","age":2}
复制代码
2. Field mapping
struct Person: Codable {
let name: String
let age: Int
let countryName: String
private enum CodingKeys: String, CodingKey {
case countryName = "country"
case name
case age
}
}
let json = #" {"name":"Tom", "age": 2, "country": "China"} "#
let person = try JSONDecoder().decode(Person.self, from: json.data(using: .utf8)!)
print(person) //Person(name: "Tom", age: 2, countryName: "China")
复制代码
After Swift 4.1, the attributeJSONDecoder
was added . If the backend uses the underlined, snake-like nomenclature, by setting the value of the attribute to , there is no need to write additional code to handle the mapping:keyDecodingStrategy
keyDecodingStrategy
convertFromSnakeCase
var decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
// 后端 person_name
// 映射为 personName
复制代码
3. Parsing the enumeration value
There is a pitfall in parsing enumeration values. For example, you only have three case
:
enum TestEnum: String {
case a
case b
case c
}
复制代码
Then one day the server added a few more things that you didn’t deal with case
, for example, if you add one case d
, then it will collapse directly after parsing in this way, so we need to find a way to case
set . The specific implementation is like this, add a protocol:
protocol CodableEnumeration: RawRepresentable, Codable where RawValue: Codable {
static var defaultCase: Self { get }
}
extension CodableEnumeration {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
let decoded = try container.decode(RawValue.self)
self = Self.init(rawValue: decoded) ?? Self.defaultCase
} catch {
self = Self.defaultCase
}
}
}
复制代码
Then case
when you can specify a by making it Enum
obey CodableEnumeration
, and then implementing defaultCase
the method case
.
/**
{
"test_enum" : "d"
}
*/
enum TestEnum: String, CodableEnumeration {
static var defaultCase: TestEnum {
.a
}
case a
case b
case c
}
struct Test: Codable {
var testEnum: TestEnum
private enum CodingKeys: String, CodingKey {
case testEnum = "test_enum"
}
}
let testJson = #" {"test_enum":"d"} "#
let test = try JSONDecoder().decode(Test.self, from: testJson.data(using: .utf8)!)
print(test.testEnum) // a
复制代码
4. Parsing nested types
Swift4 支持条件一致性,所以当数组中的每个元素都遵从 Codable 协议,字典中对应的 key
和 value
遵从 Codable 协议,整体对象就遵从 Codable 协议,就是保证你嵌套的类型都遵从 Codable 协议即可。
struct Student: Codable {
var id: String
var name: String
var grade: Int
}
struct Class: Codable {
var classNumber: String
var students: [Student]
}
/**
{
"classNumber": "111",
"students": [{
"id": "1",
"name": "studentA",
"grade": 1
}, {
"id": "2",
"name": "studentB",
"grade": 2
}]
}
*/
let classJson = #"{"classNumber":"111","students":[{"id":"1","name":"studentA","grade":1},{"id":"2","name":"studentB","grade":2}]}"#
let classModel = try JSONDecoder().decode(Class.self, from: classJson.data(using: .utf8)!)
print(classModel)
/** 输出
▿ Class
- classNumber : "111"
▿ students : 2 elements
▿ 0 : Student
- id : "1"
- name : "studentA"
- grade : 1
▿ 1 : Student
- id : "2"
- name : "studentB"
- grade : 2
*/
复制代码
5.解析空值,为null时指定默认值
后端的接口返回的数据可能有值也可能是为空的,这个时候可以将属性设置为可选类型:
/**
{
"name": "xiaoming",
"age": 18,
"partner": null
}
*/
struct Person: Codable {
let name: String
let age: Int
let partner: String?
}
let personJson = #"{"name":"xiaoming","age":18,"partner":null}"#
let person = try JSONDecoder().decode(Person.self, from: personJson.data(using: .utf8)!)
print(person) // Person(name: "xiaoming", age: 18, partner: nil)
复制代码
如果不想使用可选类型,可以重写 init(from decoder: Decoder) throws
方法,来指定一个默认的值:
struct Person: Codable {
var name: String
var age: Int
var partner: String
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
age = try container.decode(Int.self, forKey: .age)
name = try container.decode(String.self, forKey: .name)
partner = try container.decodeIfPresent(String.self, forKey: .partner) ?? ""
}
}
let personJson = #"{"name":"xiaoming","age":18,"partner":null}"#
let person = try JSONDecoder().decode(Person.self, from: personJson.data(using: .utf8)!)
print(person) // Person(name: "xiaoming", age: 18, partner: "")
复制代码
现在只有三个属性,如果属性很多,那写起来就非常麻烦,好在我们有更好的方案: Annotating properties with default decoding values 简单来说就是通过 @propertyWrapper
来进行优化,在需要默认值的属性上对这个属性进行声明,编译器就可以自动帮助我们完成赋默认值的操作,当然,这个属性包裹的实现要我们自己去实现,代码我就直接贴出来,新建个类放到项目中即可:
protocol DecodableDefaultSource {
associatedtype Value: Decodable
static var defaultValue: Value { get }
}
enum DecodableDefault {}
extension DecodableDefault {
@propertyWrapper
struct Wrapper<Source: DecodableDefaultSource> {
typealias Value = Source.Value
var wrappedValue = Source.defaultValue
}
}
extension DecodableDefault.Wrapper: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
wrappedValue = try container.decode(Value.self)
}
}
extension KeyedDecodingContainer {
func decode<T>(_ type: DecodableDefault.Wrapper<T>.Type,
forKey key: Key) throws -> DecodableDefault.Wrapper<T> {
try decodeIfPresent(type, forKey: key) ?? .init()
}
}
extension DecodableDefault {
typealias Source = DecodableDefaultSource
typealias List = Decodable & ExpressibleByArrayLiteral
typealias Map = Decodable & ExpressibleByDictionaryLiteral
enum Sources {
enum True: Source { static var defaultValue: Bool { true } }
enum False: Source { static var defaultValue: Bool { false } }
enum EmptyString: Source { static var defaultValue: String { "" } }
enum EmptyList<T: List>: Source { static var defaultValue: T { [] } }
enum EmptyMap<T: Map>: Source { static var defaultValue: T { [:] } }
enum Zero: Source { static var defaultValue: Int { 0 } }
}
}
extension DecodableDefault {
typealias True = Wrapper<Sources.True>
typealias False = Wrapper<Sources.False>
typealias EmptyString = Wrapper<Sources.EmptyString>
typealias EmptyList<T: List> = Wrapper<Sources.EmptyList<T>>
typealias EmptyMap<T: Map> = Wrapper<Sources.EmptyMap<T>>
typealias Zero = Wrapper<Sources.Zero>
}
extension DecodableDefault.Wrapper: Equatable where Value: Equatable {}
extension DecodableDefault.Wrapper: Hashable where Value: Hashable {}
extension DecodableDefault.Wrapper: Encodable where Value: Encodable {
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(wrappedValue)
}
}
复制代码
如果需要添加更多的默认值,在 Sources 中添加即可,上面的 Zero
就是额外加的,可以验证一下:
/**
{
"name":null,
"money":null,
"skills":null,
"teachers":null
}
*/
struct Person: Codable {
@DecodableDefault.EmptyString var name: String
@DecodableDefault.Zero var money: Int
@DecodableDefault.EmptyList var skills: [String]
@DecodableDefault.EmptyList var teachers: [Teacher]
}
struct Teacher: Codable {
@DecodableDefault.EmptyString var name: String
}
let personJson = #"{"name":null,"money":null,"skills":null,"teachers":null}"#
let person = try JSONDecoder().decode(Person.self, from: personJson.data(using: .utf8)!)
print(person)
/** 输出
▿ Person
▿ _name : Wrapper<EmptyString>
- wrappedValue : ""
▿ _money : Wrapper<Zero>
- wrappedValue : 0
▿ _skills : Wrapper<EmptyList<Array<String>>>
- wrappedValue : 0 elements
▿ _teachers : Wrapper<EmptyList<Array<Teacher>>>
- wrappedValue : 0 elements
*/
复制代码
6.编码
取 5 中的 person
,对它进行编码,并输出字符串或转成对应的数据格式:
// 字符串
do {
let jsonData = try JSONEncoder().encode(person)
let jsonString = String(decoding: jsonData, as: UTF8.self)
print(jsonString) // {"money":0,"teachers":[],"name":"","skills":[]}
} catch {
print(error.localizedDescription)
}
// 字典
do {
let jsonData = try JSONEncoder().encode(person)
let jsonObject = try JSONSerialization.jsonObject(with: jsonData) as? [String : Any]
print(jsonObject ?? "null")
} catch {
print(error.localizedDescription)
}
复制代码
小结
大概常用的操作就是这些,使用起来还是很方便的,HandyJSON 是依赖于 Swift 的 Runtime 源码推断内存规则,如果规则改变,那么 HandyJSON 就不管用了,而 HandyJSON 现在也不再进行维护,所以在规则没有改变之前,我们需要逐步弃用 HandyJSON 改用原生的 Codable。