【Swift】 Codable解析以及需要注意的问题

一、前言

Codable 是随 Swift 4.0 推出的,旨在取代现有的 NSCoding 协议,支持结构体、枚举和类,能将 JSON这种弱数据类型转换成代码中使用的强数据类型。

HandyJSONCodable 推出后已经不再进行维护了,而我们的项目就是依赖于 HandyJSON 来处理 JSON 的序列化反序列化,所以需要逐步迁移到Codable以避免一些错误。

本文主要介绍一些json转模型的基本概念和我使用过程中遇到的一些解析问题,在此分享一下。


二、基本概念

1. 序列化和反序列化

  • 序列化:将对象转换为字节序列的过程,在传递和保存对象时,保证对象的完整性和完整性,方便在网络上传输或者保存在文件中
let responseData = try? JSONSerialization.data(withJSONObject: response)
  • 反序列化:将字节序列恢复为对象的过程,重建对象,方便使用

let obj = try? JSONSerialization.jsonObject(with: data, options: .allowFragments

2. encode和decode

  • encode:将自身类型编码成其他外部类型
let data = try? JSONEncoder().encode(obj)
  • decode:将其他外部类型解码成自身类型
let obj = try? JSONDecoder().decode(T.self, from: data)

3. Codable

public typealias Codable = Decodable & Encodabl

实际上,Codable就是指的编码和解码协议

4. json转模型

(1) 在用原生Codable协议的时候,需要遵守协议Codable,结构体,枚举,类都可以遵守这个协议,一般使用struct

	struct UserModel:Codable {
    	var name:   String?
	    var age:    Int?
    	var sex:    Bool?
	    var name_op: String?
	}

(2) json数据里面的字段和model字段不一致

解决办法:实现 enum CodingKeys: String, CodingKey {}这个映射关系

struct UserModel:Codable {
    var name:   String?
    var age:    Int?
    var sex:    Bool?
    var name_op: String?
    var nick: String?
    enum CodingKeys: String, CodingKey {
        case name
        case age
        case sex
        case name_op 
        case nick = "nick_name" 
    }
}

上述代码中,如果后台返回的字段名称叫做nick_name,但是你想用自己命名的nick,就可以用到上述枚举中的映射去替换你想要的字段名字。
注意: CodingKeys必须字段写全,并且required init(from decoder: Decoder) throws方法必须全部解析所有字段,否则不能正确解析出正确的结果。

(3) 如果你的模型里面带有嵌套关系,比如你的模型里面有个其他模型或者模型数组,那么只要保证嵌套的模型里面依然实现了对应的协议

struct UserModel:Codable {
    var name:   String?
    var age:    Int?
    var sex:    Bool?
    var name_op: String?
    var nick: String?
    var books_op:   [BookModel]?
    enum CodingKeys: String, CodingKey {
        case name
        case age
        case sex
        case name_op 
        case nick = "nick_name" 
    }
}

// BookModel
struct BookModel:Codable {
    var name:String ?
}

(4) 使用JSONDecoder进行json转model

private func jsonToModel<T: Codable>(_ modelType: T.Type, _ response: Any) -> T? {
        guard let data = try? JSONSerialization.data(withJSONObject: response), let info = try? JSONDecoder().decode(T.self, from: data) else {
            return nil
        }
        return info
    }

先使用JSONSerialization进行一次序列化操作

看一下decode源码:

// MARK: - Decoding Values

    /// Decodes a top-level value of the given type from the given JSON representation.
    ///
    /// - parameter type: The type of the value to decode.
    /// - parameter data: The data to decode from.
    /// - returns: A value of the requested type.
    /// - throws: `DecodingError.dataCorrupted` if values requested from the payload are corrupted, or if the given data is not valid JSON.
    /// - throws: An error if any value throws an error during decoding.
    open func decode<T : Decodable>(_ type: T.Type, from data: Data) throws -> T {//泛型并且约束遵守协议
        let topLevel: Any
        do {
           //反序列化操作
            topLevel = try JSONSerialization.jsonObject(with: data, options: .allowFragments)
        } catch {
            throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: error))
        }
        let decoder = _JSONDecoder(referencing: topLevel, options: self.options)
        
        //调用unbox解码并返回解码后的数据
        guard let value = try decoder.unbox(topLevel, as: type) else {
            throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: [], debugDescription: "The given data did not contain a top-level value."))
        }

        return value
    }

可以看到在转model的时候,先进行一次序列化操作,decode内部又进行一次反序列化操作,苹果这样设计估计是在参数传递的时候想让我们传递字节流

至此就可以使用swift原生协议Codable进行json转model了

5. JSON字符串转模型

这是一个字符串

let jsonString:String = """
           {
             "name":"Tomas",
             "age":"10",
             "gender":"man"
        
           }
        """

如果将它解析成User对象

let jsonData: Data = jsonString.data(using:String.Encoding.utf8)!
let decoder = JSONDecoder()
 do {
      let user:UserModel = try decoder.decode(UserModel.self,from:jsonData)
    } catch {

}

6. JSONSerialization

Serialization 是序列化的意思,JSONSerialization 顾名思义是对JSON进行序列化。

JSONSerialization 是对 JSON 字符串进行序列化和反序列化的工具类。用这个类可以将JSON转成对象,也可以将对象转成JSON。

let dict:[String:Any] = ["name":"jack",
                          "age":10,
                       "gender":"man"]
if JSONSerialization.isValidJSONObject(dict) == false {
            return
        }
        
let data:Data = try! JSONSerialization.data(withJSONObject: dict, options: .fragmentsAllowed);
 // 将data转换成制定model
guard let Model:UserModel = try? JSONDecoder().decode(UserModel.self, from: data) else {
            return
        }
        // 将data转成字符串输出
let string = String(data: data, encoding: String.Encoding.utf8)
print(string)
print(Model.name)

Optional("{\"name\":\"jack\",\"gender\":\"man\",\"age\":10}")
Optional("jack")
  • JSON转字典
let jsonString = "{\"name\":\"jack\",\"age\":10}";
let data = jsonString.data(using: .utf8)
let dict = try! JSONSerialization.jsonObject(with: data!, options: .allowFragments)
        print(dict)

三、Codable 解码设置默认值

References:
Swift - Codable 解码设置默认值

四、常见问题

1. 类型的某个属性有默认值,后台返回的JSON没有这个属性对应的数据

(1) 正常的Demo

假设我们有一个User类型,有一个id属性,和一个是否被当前用户关注的属性isFollowedByCurrentUser,并实现了Codable协议,代码如下:

struct User: Codable {
    var id: String
    var isFollowedByCurrentUser: Bool?

    enum CodingKeys: String, CodingKey {
        case id
        case isFollowedByCurrentUser = "followed"
    }
}

我们的JSON数据如下:

let jsonString = """
  {
    "id":"efa41bae-25fa-428b-99c1-6d3c1b178875",
    "followed": true
  }
"""

JSONDecoder进行解码:

let decoder = JSONDecoder()

let data = jsonString.data(using: .utf8)!

do {
    let user = try decoder.decode(User.self, from: data)
    print(user)
} catch {
    print("error: \(error)")
}

毫无疑问,上面的代码是可以解码成功的。

(2) 失败的Demo

有些时候,后台返回的JSON数据可能缺少某些字段,假设缺少了followed,那么现在的JSON数据为:

let jsonString = """
  {
    "id":"efa41bae-25fa-428b-99c1-6d3c1b178875"
  }
"""

这时我们用上面的JSONDecoder进行解码,也是可以解码成功的,只不过isFollowedByCurrentUser的值为nil而已。

现在问题来了,我们看回User类型。通常我们在某个类型添加一个Bool属性时,一般会给他一个默认值false,所以我们会习惯的把User写成:

struct User: Codable {

    var id: String
    var isFollowedByCurrentUser = false

    enum CodingKeys: String, CodingKey {
        case id
        case isFollowedByCurrentUser = "followed"
    }
}

这时如果我们再用JSONDecoder把缺少followed字段的JSON数据转成User的话,是无法转成功的,错误如下:

error: keyNotFound(CodingKeys(stringValue: “followed”, intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: “No value associated with key CodingKeys(stringValue: “followed”, intValue: nil) (“followed”).”, underlyingError: nil))

JSONDecoderJSON数据中无法找到followed对应的值。


(3) 解决办法:

我们无法保证服务器总是返回完整正确的数据,所以只能从我们客户端去解决问题。

1. 把类型的所有属性都定义为Optional类型

这是最简单方便的方法。这样解码的时候,JSONDecoder发现JSON没有对应的数据,就自动把这个属性设置为nil

2. 实现Decodable的初始化函数,并使用decodeIfPresent来解码

正常情况下,我们定义了CodingKeys之后,不需要手动实现init(from decoder: Decoder) throws这个初始化函数的,JSONDecoder就可以正常解码。但是我们把isFollowedByCurrentUser定义成一个非可选类型,我们必须实现这个初始化函数,才能正常解码。明确这一点非常重要!!!

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    id = try container.decode(String.self, forKey: .id)
    isFollowedByCurrentUser = try container.decodeIfPresent(Bool.self, forKey: .isFollowedByCurrentUser) ?? false
}

五、References

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值