0%

Swift 数据模型转换框架比较

Xcode11.3.1,Swift5环境.

在App开发中经常需要将服务端返回的JSON数据转化为模型使用,选择一个好的数据模型转换框架有助于提高开发体验以及减少转换过程中的错误.

我们希望框架支持纯Swift类,结构体,OC类,因此YYModel和MJExtension等一系列优秀的OC数据模型转换框架是不满足要求的,因为它们都需要模型继承自NSObject.

目前使用较多的数据模型转换框架有:

  1. 系统自带的基于Codable的JSONDecoder/JSONEncoder.
  2. HandyJSON
  3. ObjectMapper

下面简单介绍一下各框架的使用,并从框架的使用便捷性和数据容错性上进行一个比较.

JSONDecoder/JSONEncoder

介绍JSONDecoder之前,先来看下Codable是什么.

Codable是Swift 4.0开始引入的新特性.Codable定义:public typealias Codable = Decodable & Encodable,可以看到Codable是一个类型别名,代表同时遵守Decodable和Encodable协议.

Decodable协议定义了一个解码函数:

init(from decoder: Decoder) throws

遵从Decodable协议的类型可以使用任何遵守了Decoder协议的对象进行初始化,完成一个解码过程。

Encodable协议定义了一个编码函数:

func encode(to encoder: Encoder) throws

遵从Encodable协议的类型可以使用任何遵守了Encoder协议的对象进行编码,完成一个编码过程。

可以看到Codable只是规定了编解码的一般规则,并不是为某一个具体的数据格式协议设计的.因此它的扩展性是很强的.系统提供的JSONDecoder,JSONEncoder就是对应于具体的JSON数据格式的编解码类.类似的还有PropertyListEncoder,PropertyListDecoder.当然我们也可以和后端商定一种自定义的数据格式,然后定义两个类分别实现Decoder和Encoder协议用于编解码.

注意:JSONEncoder自身并没有实现Encoder协议,而是它内部的另一个类_JSONEncoder实现的.JSONEncoder起到一个封装隐藏细节的作用.

JSONDecoder的解码方法:

open func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable

被解码的类型需要遵守Decodable协议.

JSONEncoder的编码方法:

open func encode<T>(_ value: T) throws -> Data where T : Encodable

被编码的类型需要遵守Encodable协议.

一个模型遵守Codable协议后,通过JSONDecoder,JSONEncoder我们就可以进行JSON的序列化与反序列化.

基本使用

json:

1
2
3
4
5
{
"id": "123456",
"name": "小明",
"grade": 1
}

模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
struct Student1: Decodable, Encodable {
var id: String
var name: String
var grade: Int

init(id: String, name: String, grade: Int) {
self.id = id
self.name = name
self.grade = grade
}

private enum CodingKeys: String, CodingKey {
case id
case name
case grade
}

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

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: CodingKeys.id)
name = try container.decode(String.self, forKey: CodingKeys.name)
grade = try container.decode(Int.self, forKey: CodingKeys.grade)
}
}

为了能够编解码,我们让模型Student1遵守Decodable, Encodable协议,并实现协议.

这样就可以使用JSONEncoder/JSONDecoder编解码了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func testBaseCodable() {
let deJsonStr = """
{
"id": "123456",
"name": "小明",
"grade": 1
}
"""
let deData = deJsonStr.data(using: String.Encoding.utf8)!
do {
let decoder = JSONDecoder.init()
let object = try decoder.decode(Student1.self, from: deData)
DLog(object)
} catch let error as NSError {
DLog("解码失败:\(error.debugDescription)")
}

let student = Student1.init(id: "127182781278", name: "小红", grade: 3)
do {
let jsonData2 = try JSONEncoder.init().encode(student)
let jsonString = String.init(data: jsonData2, encoding: String.Encoding.utf8)
DLog(jsonString!)
} catch let error as NSError {
DLog("编码失败:\(error.debugDescription)")
}
}

上述代码中我们为模型实现了Codable协议.在实际使用时并不需要这么麻烦,因为Swift标准库中的类型,比如String,Int,Double和 Foundation 框架中Data,Date,URL都是默认支持Codable协议的,所以如果你的模型使用的都是Swift标准库中的类型,那么只需声明支持协议即可。如下:

1
2
3
4
5
struct Student: Codable {
var id: String
var name: String
var grade: Int
}

然后就可以通过JSONDecoder将json数据转换为模型了.是不是很方便?

实际上,在编译代码时系统会根据类型的属性,自动生成了一个 CodingKeys 的枚举类型定义,这是一个以 String 类型作为原始值的枚举类型,对应每一个属性的名称。然后再给每一个声明实现 Codable协议的类型自动生成 init(from:)encode(to:) 两个函数的具体实现,最终完成了整个协议的实现。

嵌套对象,数组和字典

一般情况下会遇到嵌套的情景.这个时候该如何处理呢?

需要像YYModel那样实现方法+ (nullable NSDictionary<NSString *, id> *)modelContainerPropertyGenericClass;吗?

我们看一下Array和Dictionary的扩展:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
extension Array : Encodable where Element : Encodable {
public func encode(to encoder: Encoder) throws
}

extension Array : Decodable where Element : Decodable {
public init(from decoder: Decoder) throws
}

extension Dictionary : Decodable where Key : Decodable, Value : Decodable {
public init(from decoder: Decoder) throws
}

extension Dictionary : Encodable where Key : Encodable, Value : Encodable {
public func encode(to encoder: Encoder) throws
}

当数组中每个元素都遵从Codable协议,字典中对应的key和value遵从Codable协议,那么整个容器对象也就遵从Codable协议.

1
2
3
4
5
6
7
8
9
10
11
12
struct Class: Codable {
var className: String
var students: Array<Student>
var teacher: Person0
}

class Person0: Codable {
var name: String = ""
var age: Int = 0
var motto: String = ""
var sex: Bool = true
}

因为Student遵守Codable协议,因此Array<Student>容器也就遵守Codable协议.这样Class的每个属性都遵守Codable协议.因此如前面所说只需声明Class支持Codable协议即可.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"className":"3年A組",
"students":[
{
"id": "13321",
"name": "小明",
"grade": 1
},
{
"id": "213",
"name": "小王",
"grade": 1
}
],
"teacher": {
"name": "张三丰",
"age": 23,
"motto": "nothing is impossible.",
"sex": false
}
}

解码:

1
2
3
let deData = deJsonStr.data(using: String.Encoding.utf8)!
let object = try! JSONDecoder.init().decode(Class.self, from: deData)
print(object)

键名和属性名不一致

一般会遇到和服务器字段名定义不一致的情况.

json如下:

1
2
3
4
5
6
{
"_name": "张三丰",
"_age": 23,
"motto": "nothing is impossible.",
"sex": false
}

模型如下:

1
2
3
4
5
6
class Person1: Codable {
var name: String = ""
var age: Int = 0
var motto: String = ""
var sex: Bool = true
}

如果不做任何处理会报错:

1
keyNotFound(CodingKeys(stringValue: "name", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"name\", intValue: nil) (\"name\").", underlyingError: nil))

解决办法:

定义一个遵守CodingKey 协议的枚举CodingKeys(名字必须是CodingKeys)指定一个明确的映射。Swift 会寻找遵守CodingKey 协议的名为 CodingKeys 的子类型。解码时Swift只解码这里面的属性,其他属性会被忽略.

如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person1: Codable {
var name: String = ""
var age: Int = 0
var motto: String = ""
var sex: Bool = true

enum CodingKeys: String, CodingKey {
case name = "_name"
case age = "_age"
case motto = "motto"
case sex = "sex"
}
}

坑点:只要有一个字段名称不匹配,就得定义一个遵守CodingKey 协议的枚举并把其他属性名称全部写进枚举里.

键值对和属性数量不一致

  1. 服务器少于客户端字段情况.
1
2
3
4
{
"name": "张三丰",
"age": 23
}

如果不做处理会报错:

1
keyNotFound(CodingKeys(stringValue: "motto", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"motto\", intValue: nil) (\"motto\").", underlyingError: nil))

如果是服务器少返回了某个字段,

解决办法:将该字段定义为可选类型.

1
2
3
4
5
6
class Person21: Codable {
var name: String = ""
var age: Int = 0
var motto: String? = ""
var sex: Bool? = true
}

如果是模型中定义了一些其他字段而服务器没有的,

解决办法:将该字段定义为可选类型或者定义一个遵守CodingKey 协议的枚举,把需要解码的属性写上即可,如下:

1
2
3
4
5
6
7
8
9
10
11
class Person2: Codable {
var name: String = ""
var age: Int = 0
var motto: String = ""
var sex: Bool = true

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

坑点:有的后台开发者会将没有值的字段省略,所以你根本不知道哪个字段会不返回,不可能把每一个字段都声明为可选值,否则你在使用的过程中简直是噩梦.

  1. 服务器多于客户端字段情况

服务器返回了许多我们不需要的字段,这种情况下,模型只需要定义好该有的属性,无需定义一个遵守CodingKey 协议的枚举,Codable解码时会自动忽略掉多余的键值.

总体上json键值对要包含模型的属性.属于包含单映射关系.

类型不一致

服务器返回的是String但客户端定义的是Int.

json如下:

1
2
3
4
5
6
7
{
"name": "张三丰",
"age": 23,
"motto": "nothing is impossible.",
"sex": false,
"height": 175
}

模型如下:

1
2
3
4
5
6
class Person3: Codable {
var name: String = ""
var age: Int = 0
var motto: String = ""
var sex: Bool = true
}

如果不做任何处理则报错:类型不匹配.

1
typeMismatch(Swift.Int, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "age", intValue: nil)], debugDescription: "Expected to decode Int but found a string/data instead.", underlyingError: nil))

解决办法:

Codable 踩坑

Bool值解析

Swift里Bool值只有true和false,如果后端返回的是0,1这样的整型值,Codable会解析出错:类型不匹配.

1
typeMismatch(Swift.Bool, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "sex", intValue: nil)], debugDescription: "Expected to decode Bool but found a number instead.", underlyingError: nil))

枚举值

  1. 服务端使用整型枚举

  2. 服务端使用字符串枚举

json如下:

1
2
3
4
5
{
"feedID":"100000",
"template": "video",
"gender": 1
}

template有:video,pic,link.

gender有:0,1.

模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class TimeLine: Codable {

// 服务端使用字符串枚举
enum TimeLineTemplate: String, Codable {
case video = "video"
case pic = "pic"
case link = "link"
}

//服务端使用整型枚举
enum Gender: Int, Codable {
case female = 0
case male = 1
}

var feedID: String
var template: TimeLineTemplate
var gender: Gender
}

解决办法:让枚举类型支持 Codable 协议.

需要将枚举声明为具有原始值的形式,并且原始值的类型需要支持 Codable 协议.

  1. 定义一个具有原始值的枚举,指定原始值类型为String或Int.
  2. 让枚举遵守Codable协议.

需要注意的是:枚举的原始值必须和服务器端商定好保持一致.比如原本只有0,1两个枚举值,但后台却返回3.这就没法对应上了.解码会报错:

1
dataCorrupted(Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "template", intValue: nil)], debugDescription: "Cannot initialize TimeLineTemplate from invalid String value videos", underlyingError: nil))
1
dataCorrupted(Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "gender", intValue: nil)], debugDescription: "Cannot initialize Gender from invalid Int value 4", underlyingError: nil))

模型中某个属性的类型不支持Codable协议

比如model使用了一些不属于我们自己的类型,该类型又没有遵守Codable协议.如何让model支持Codable?

比如CLLocationCoordinate2D并没有实现Codable协议.

解决办法有三种:个人感觉无论哪一种都不方便.

  1. 手动让CLLocationCoordinate2D支持Codable协议.

  2. 手动让自身支持Codable协议.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Person5: Codable {
var name: String = ""
var age: Int = 0
var motto: String = ""
var sex: Bool = true
var position: CLLocationCoordinate2D?

private enum CodingKeys: String, CodingKey {
case name = "_name"
case age = "_age"
case motto
case sex
case position = "position"
}

//自己实现Codable协议.缺点:麻烦,编码,解码都需要实现.
private enum PositionCodingKeys: String, CodingKey {
case latitude
case longitude
}

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

required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
self.age = try container.decode(Int.self, forKey: .age)
self.motto = try container.decode(String.self, forKey: .motto)
self.sex = try container.decode(Bool.self, forKey: .sex)

let posContainer = try container.nestedContainer(keyedBy: PositionCodingKeys.self, forKey: .position)
self.position = CLLocationCoordinate2D.init(latitude: try posContainer.decode(Double.self, forKey: .latitude), longitude: try posContainer.decode(Double.self, forKey: .longitude))
}

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

var posContainer = container.nestedContainer(keyedBy: PositionCodingKeys.self, forKey: .position)
try posContainer.encode(position?.latitude, forKey: .latitude)
try posContainer.encode(position?.latitude, forKey: .longitude)
}
}
  1. 给CLLocationCoordinate2D单独创建一个支持Codable的包装类.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Person4: Codable {
var name: String = ""
var age: Int = 0
var motto: String = ""
var sex: Bool = true
var id: String = ""

var position: CLLocationCoordinate2D? {
get {
return _position == nil ? nil : CLLocationCoordinate2D.init(latitude: _position!.latitude, longitude: _position!.longitude)
}
set {
if newValue == nil {
_position = nil
} else {
_position = _CLLocationCoordinate2D.init(latitude: newValue!.latitude, longitude: newValue!.longitude)
}
}
}

private struct _CLLocationCoordinate2D: Codable {
var latitude: CLLocationDegrees
var longitude: CLLocationDegrees

init(latitude: CLLocationDegrees, longitude: CLLocationDegrees) {
self.latitude = latitude
self.longitude = longitude
}
}
private var _position: _CLLocationCoordinate2D?

private enum CodingKeys: String, CodingKey {
case name = "_name"
case age = "_age"
case motto
case sex
case _position = "position"
}
}

编解码有继承关系的模型

默认情况下,只有继承来的属性有值,子类自身的属性将不会有值.

模型如下:

1
2
3
4
5
6
7
8
9
10
class Person0: Codable {
var name: String = ""
var age: Int = 0
var motto: String = ""
var sex: Bool = true
}

class Teacher: Person0 {
var course: String = ""
}

如果写为:class Teacher: Person0, Codable,编译会出错:Redundant conformance of ‘Teacher’ to protocol ‘Decodable’.因为Teacher的父类Person0已经遵守Codable了.

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
func codablePolymorphismType() {
let deJsonStr = """
{
"name": "张三丰",
"age": 23,
"motto": "nothing is impossible.",
"sex": true
}
"""
let deData = deJsonStr.data(using: String.Encoding.utf8)!

let decoder = JSONDecoder.init()
do {
let object = try decoder.decode(Person0.self, from: deData)
DLog(object)
} catch {
DLog(error)
}

let deTeacherJsonStr = """
{
"name": "张三丰",
"age": 23,
"motto": "nothing is impossible.",
"sex": true,
"course": "语文"
}
"""
let deTeacherData = deTeacherJsonStr.data(using: String.Encoding.utf8)!

var object: Teacher?
do {
object = try JSONDecoder.init().decode(Teacher.self, from: deTeacherData)
DLog(object!)
} catch {
DLog(error)
}

if let teacher = object {
do {
let data = try JSONEncoder.init().encode(teacher)
let jsonString = String.init(data: data, encoding: String.Encoding.utf8)
DLog(jsonString!)
} catch let error as NSError {
DLog("编码失败:\(error.debugDescription)")
}
}
}

以上解码时,Teacher对象继承的父类的属性有值但自身course属性将是nil.对于这种有继承关系的模型的编解码该怎么办呢?

解决办法:回到最开始的基本使用,自己手动实现Codable协议.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Person0: Codable {
var name: String = ""
var age: Int = 0
var motto: String = ""
var sex: Bool = true
}

class Teacher: Person0 {
var course: String = ""

enum CodingKeys: String, CodingKey {
case course
}

required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
course = try container.decode(String.self, forKey: .course)
try super.init(from: decoder)
}

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

坑点:使用不便.

编码Any

JSONDecoder/JSONEncoder总结

优势
  1. 系统支持,兼容性问题小.
  2. 如果模型中都是基础类型,JSONDecoder/JSONEncoder更方便.
  3. 对原生类型支持更好
不足
  1. 只要有一个属性解析失败则直接抛出异常导致整个解析过程失败。主要体现在:
  • json中出现null值,空对象,字段缺失,如果不做处理,当解析到该字段时系统会报错.

    1
    2
    解码失败:Error Domain=NSCocoaErrorDomain Code=4865 "No value associated with key CodingKeys(stringValue: "grade", intValue: nil) ("grade")." UserInfo={NSDebugDescription=No value associated with key CodingKeys(stringValue: "grade", intValue: nil) ("grade")., NSCodingPath=(
    )}
    1
    2
    3
    解码失败:Error Domain=NSCocoaErrorDomain Code=4865 "Expected Int value but found null instead." UserInfo={NSCodingPath=(
    "CodingKeys(stringValue: \"grade\", intValue: nil)"
    ), NSDebugDescription=Expected Int value but found null instead.}

    解决办法:将模型中该属性设置为可选值.但正如前文分析的那样,有的后台开发者会将没有值的字段省略,所以你根本不知道哪个字段会不返回,不可能把每一个字段都声明为可选值,否则你在使用的过程中简直是噩梦.

  • 类型不匹配时,如果不做处理,当解析到该字段时系统会报错.

    比如服务端定义为String,而你定义为Int.

    浅谈 Swift JSON 解析:主流 JSON 解析框架比较,分析了Codable的不足,并提出了解决方案CleanJSON

  1. 只要有一个字段名称和后台不一致,就得定义一个遵守CodingKey 协议的枚举并把其他属性名称全部写进枚举里,如果一个模型有很多属性,会很繁琐.
  2. 编解码有继承关系的模型不方便,需要手动实现Codable协议.

HandyJSON

alibaba开源的一个 json-object 互转的框架.

HandyJSON原理

主要步骤:

  1. 通过某种机制(比如Mirror)获取属性名、类型.
  2. 根据属性名从JSON串中解析值.
  3. 找到实例在内存中的 headPointer, 通过属性的类型计算内存中的偏移值, 确定属性在内存中的位置.
  4. 在内存中为属性赋值.(这一步会进行类型的容错处理,比如字符串转数字,数字转字符串等一些简单的转换)

由于这个类实例的内存布局可能会变,所以官方文档中有提到”HandyJSON is totally depend on the memory layout rules infered from Swift runtime code. We are watching it and will follow every bit if it changes.“.

为啥要分析类实例的内存布局规则呢?

我们知道OC里面可以通过runtime获取一个类所有的属性名和类型,然后通过KVC就可以完成赋值.但是Swift的反射功能很弱,Swift的反射实现类Mirror它只能在运行时获取一个Model实例的所有字段、字段值,但却无法给它赋值,缺少一个类似OC里面KVC的这么一个东西,所以HandyJSON才需要分析类实例的内存布局规则,根据布局规则直接在内存中为属性赋值.

具体可以参考HandyJSON 设计思路简析.

HandyJSON使用

HandyJSON的使用官方文档其实已经很全面了.

下面通过一个例子将上面所有的情况也测试一下:

模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class HJPerson: HandyJSON  {

//服务端使用整型枚举
enum Hobbit: Int, HandyJSONEnum {
case none = -1
case basketball = 0
case football = 1
}

enum Music: String, HandyJSONEnum {
case none
case classic
case modern
}

var name: String = ""
var age: Int = 0
var motto: String = ""
var sex: Bool = true
var height: Float = 0
var id: String = ""
var hobbit: Hobbit = .none
var music: Music = .none

required init() {

}

//名称不匹配时
func mapping(mapper: HelpingMapper) {
mapper.specify(property: &name, name: "_name")
}
}

class HJTeacher: HJPerson {
var course: String = ""
}

struct Student2: HandyJSON {
init() {
id = ""
name = ""
grade = 0
}

var id: String
var name: String
var grade: Int
}

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
func handyJsonStudent() {
let deJsonStr = """
{
"id": "2323",
"name": "小明"
}
"""
// let student = JSONDeserializer<Student2>.deserializeFrom(json: deJsonStr)
let student = Student2.deserialize(from: deJsonStr)
DLog(student)
}

private func handyJsonKeyPropertyTypeNotMatch() {
let deJsonStr = """
{
"_name": "张三丰",
"age": "23",
"motto": "nothing is impossible.",
"sex": 1,
"height": "172.4",
"hobbit": "1",
"music": "modern",
"id": 345345131
}
"""
let student = HJPerson.deserialize(from: deJsonStr)
DLog(student)
}

func handyJsonPolymorphismType() {
let deJsonStr = """
{
"_name": "张三丰",
"age": 23,
"motto": "nothing is impossible.",
"hobbit": "4",
"sex": true
}
"""
let person = HJPerson.deserialize(from: deJsonStr)
DLog(person)

let deTeacherJsonStr = """
{
"_name": "张三丰",
"age": 23,
"motto": "nothing is impossible.",
"sex": true,
"course": "语文"
}
"""
let teacher = HJTeacher.deserialize(from: deTeacherJsonStr)
DLog(teacher!)

let string = teacher!.toJSONString()
DLog(string!)
}

当服务端返回的JSON格式不规范时,比如名称不匹配,类型不匹配,缺失字段,字段值为null,Bool值问题等,使用HandyJSON基本上不需要额外的处理,非常方便.

HandyJSON优点

  1. 使用非常方便
  2. 数据容错性高

HandyJSON缺点

  1. 依赖于类实例的内存布局规则.
  2. Swift版本兼容性问题.一旦出问题就只能等官方出新版本了.Swift4之前比较坑,不过现在都Swift5了,这个问题会好点.

总的来说还是值得入坑的.

ObjectMapper

应该是比较早的一批转换框架了,使用上类似 Codable,但是需要额外写 map 方法,重复劳动过多。不是很推荐.

模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class OMPerson: Mappable   {
//服务端使用整型枚举
enum Hobbit: Int {
case none = -1
case basketball = 0
case football = 1
}

enum Music: String {
case none
case classic
case modern
}

var name: String = ""
var age: Int = 0
var motto: String = ""
var sex: Bool = true
var height: Float = 0
var id: String = ""
var hobbit: Hobbit = .none
var music: Music = .none

required init?(map: Map) {

}

func mapping(map: Map) {
name <- map["_name"]
age <- map["age"]
motto <- map["motto"]
sex <- map["sex"]
height <- map["height"]
id <- map["id"]
hobbit <- map["hobbit"]
music <- map["music"]
}
}

class OMTeacher: OMPerson {
var course: String = ""

required init?(map: Map) {
super.init(map: map)
}

override func mapping(map: Map) {
super.mapping(map: map)
course <- map["course"]
}
}

需要遵守并实现Mappable协议的俩函数,尤其是func mapping(map: Map)函数,比较繁琐.类型不匹配时不会赋值给属性,不像JSONDecoder那样直接报错,但也不像HandyJSON会帮你尝试做一下转换.

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
func objectMapperStudent() {
let deJsonStr = """
{
"id": "2323",
"_name": "小明"
}
"""
let student = Student3.init(JSONString: deJsonStr)
DLog(student)
}

private func objectMapperKeyPropertyTypeNotMatch() {
let deJsonStr = """
{
"_name": "张三丰",
"age": "23",
"motto": "nothing is impossible.",
"sex": 0,
"height": "172.4",
"hobbit": 1,
"music": "modern",
"id": 345345131
}
"""
let student = OMPerson.init(JSONString: deJsonStr)
DLog(student)
}

func objectMapperPolymorphismType() {
let deJsonStr = """
{
"_name": "张三丰",
"age": 23,
"motto": "nothing is impossible.",
"hobbit": "4",
"sex": true
}
"""
let person = OMPerson.init(JSONString: deJsonStr)
DLog(person)

let deTeacherJsonStr = """
{
"_name": "张三丰",
"age": 23,
"motto": "nothing is impossible.",
"sex": true,
"course": "语文"
}
"""
let teacher = OMTeacher.init(JSONString: deTeacherJsonStr)
DLog(teacher!)

let string = Mapper.init().toJSONString(teacher!)
DLog(string!)
}

总结

JSONDecoder/JSONEncoder HandyJSON
易用性 较好
数据容错性 一般
兼容性 一般

考虑到易用性和数据容错性推荐使用HandyJSON.但是基于Codable的JSONDecoder/JSONEncoder如果能够很好的解决数据容错性方面的问题还是很不错的,毕竟可以少引入一个外部框架.

觉得文章有帮助可以打赏一下哦!