Codable Nanny Level Raiders

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.

transfer.png

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:keyDecodingStrategykeyDecodingStrategyconvertFromSnakeCase

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 caseset . 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 casewhen you can specify a by making it Enumobey CodableEnumeration, and then implementing defaultCasethe 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 协议,字典中对应的 keyvalue 遵从 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。

参考

Annotating properties with default decoding values

Encoding and Decoding Custom Types

Guess you like

Origin juejin.im/post/7122667987902922789