在Swift 4中解析和使用Codable



JSON格式非常流行,通常用于客户端-服务器应用程序中的数据传输和查询执行。 JSON解析需要这种格式的编码/解码工具,Apple最近更新了它们。 在本文中,我们将研究使用Decodable协议的JSON解析方法,将新的Codable协议与NSCoding的前身进行比较,评估优缺点,使用特定的示例进行分析,并考虑实现协议时遇到的一些功能。


什么是可编码?

在WWDC2017上,Apple与新版本的Swift 4一起引入了新的数据编码/解码工具,这些工具通过以下三种协议实现:

-可编码
-可编码
-可腐烂

在大多数情况下,这些协议用于JSON,但除此之外,它们还用于将数据保存到磁盘,通过网络传输等。 Encodable用于将Swift数据结构转换为JSON对象,而Decodable则有助于将JSON对象转换为Swift数据模型。 Codable协议结合了前两个,并且是它们的类型别名:

typealias Codable = Encodable & Decodable 


为了符合这些协议,数据类型必须实现以下方法:

可编码
编码(to :)-将数据模型编码为给定的编码器类型

可腐烂
init(from :)-从提供的解码器初始化数据模型

可编码
编码(到:)
初始化(来自:)

一个简单的用例

现在考虑一个使用Codable的简单示例,因为它同时实现了EncodableDecodable ,在此示例中,您可以立即看到所有协议功能。 假设我们拥有最简单的JSON数据结构:

 { "title": "Nike shoes", "price": 10.5, "quantity": 1 } 


使用此JSON的数据模型如下所示:
 struct Product: Codable { var title:String var price:Double var quantity:Int enum CodingKeys: String, CodingKey { case title case price case quantity } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(title, forKey: .title) try container.encode(price, forKey: .price) try container.encode(quantity, forKey: .quantity) } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) title = try container.decode(String.self, forKey: .title) price = try container.decode(Double.self, forKey: .price) quantity = try container.decode(Int.self, forKey: .quantity) } } 


两种必需的方法都实现了,还描述了枚举以确定编码/解码字段的列表。 实际上,由于Codable支持自动生成encoding (to :)和init(from :)方法以及必要的枚举,因此可以大大简化编写过程。 也就是说,在这种情况下,您可以编写如下结构:

 struct Product: Codable { var title:String var price:Double var quantity:Int } 


非常简单和简约。 仅,请不要忘记,在以下情况下,这种简洁的记录将不起作用:

- 数据模型结构与您要编码/解码的模型不同

- 您可能需要对数据模型的属性之外的其他属性进行编码/解码

- 数据模型的某些属性可能不支持Codable协议。 在这种情况下,您需要将它们从/转换为Codable协议

- 如果数据模型中的变量名称与容器中的字段名称不匹配

由于我们已经考虑了数据模型的最简单定义,因此值得举一个实际使用的小例子:

因此,您可以在一行中以JSON格式解析服务器响应:
 let product: Product = try! JSONDecoder().decode(Product.self, for: data) 


相反,以下代码将从数据模型创建JSON对象:
 let productObject = Product(title: "Cheese", price: 10.5, quantity: 1) let encodedData = try? JSONEncoder().encode(productObject) 


一切都非常方便快捷。 正确描述了数据模型并使它们成为Codable之后 ,您就可以在一行中直接对数据进行编码/解码。 但是我们考虑了最简单的数据模型,其中包含少量简单类型的字段。 考虑可能的问题:

并非数据模型中的所有字段都是可编码的。

为了使您的数据模型实现Codable协议模型的所有字段都必须支持此协议。 默认情况下, Codable协议支持以下数据类型: 字符串, 整数 ,双 精度,数据,URLCodable还支持Array,Dictionary,Optional ,但前提是它们包含Codable类型。 如果数据模型的某些属性与Codable不对应,则必须将它们带到该属性。

 struct Pet: Codable { var name: String var age: Int var type: PetType enum CodingKeys: String, CodingKey { case name case age case type } init(from decoder: Decoder) throws { . . . } func encode(to encoder: Encoder) throws { . . . } } 


如果在我们的Codable数据模型中使用了一个自定义类型(例如PetType) ,并且想要对其进行编码/解码,那么它还必须实现其init并进行编码。

数据模型与JSON字段不匹配

例如,如果在数据模型中定义了3个字段,并且在JSON对象中获得了5个字段,其中有2个是这3个字段的附加字段,那么在解析过程中什么都不会改变,您只需从这5个字段中取出3个字段即可。这种情况,并且在JSON对象中,数据模型中至少会有一个字段,因此会发生运行时错误。
如果某些字段是可选的,并且在JSON对象中定期不存在,那么在这种情况下,有必要将它们设置为可选:

 class Product: Codable { var id: Int var productTypeId: Int? var art: String var title: String var description: String var price: Double var currencyId: Int? var brandId: Int var brand: Brand? } 


使用更复杂的JSON结构

服务器响应通常是一个实体数组,例如,您请求一个商店列表并以以下形式获得答案:

 { "items": [ { "id": 1, "title": " ", "link": "https://www.youtube.com/watch?v=Myp6rSeCMUw", "created_at": 1497868174, "previewImage": "http://img.youtube.com/vi/Myp6rSeCMUw/mqdefault.jpg" }, { "id": 2, "title": "  2", "link": "https://www.youtube.com/watch?v=wsCEuNJmvd8", "created_at": 1525952040, "previewImage": "http://img.youtube.com/vi/wsCEuNJmvd8/mqdefault.jpg" } ] } 

在这种情况下,您可以简单地将其编写和解码为Shop实体数组。

 struct ShopListResponse: Decodable { enum CodingKeys: String, CodingKey { case items } let items: [Shop] } 


在此示例中,自动函数init将起作用,但是如果您想自己编写解码,则需要将解码类型指定为数组:

 self.items = try container.decode([Shop].self, forKey: .items) 


Shop结构还应该分别实现Decodable协议

 struct Shop: Decodable { var id: Int? var title: String? var address: String? var shortAddress: String? var createdAt: Date? enum CodingKeys: String, CodingKey { case id case title case address case shortAddress = "short_address" case createdAt = "created_at" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.id = try? container.decode(Int.self, forKey: .id) self.title = try? container.decode(String.self, forKey: .title) self.address = try? container.decode(String.self, forKey: .address) self.shortAddress = try? container.decode(String.self, forKey: .shortAddress) self.createdAt = try? container.decode(Date.self, forKey: .createdAt) } } 


解析此元素数组将如下所示:

 let parsedResult: ShopListResponse = try? JSONDecoder().decode(ShopListResponse.self, from: data) 


因此,您可以轻松使用数据模型数组,并在其他模型中使用它们。

日期格式

在此示例中,还有一个细微差别,在这里我们首先遇到了Date类型的使用。 使用此类型时,日期编码可能存在问题,通常此问题与后端一致。 默认格式为.deferToDate

 struct MyDate : Encodable { let date: Date } let myDate = MyDate(date: Date()) try! encoder.encode(foo) 


myDate将如下所示:
 { "date" : 519751611.12542897 } 


例如,如果需要使用.iso8601格式,则可以使用dateEncodingStrategy属性轻松更改格式:

 encoder.dateEncodingStrategy = .iso8601 


现在,日期将如下所示:

 { "date" : "2017-06-21T15:29:32Z" } 

您还可以使用自定义日期格式,甚至可以使用以下格式选项编写自己的日期解码器:

.formatted(DateFormatter) -它自己的日期解码器格式
.custom((Date,Encoder)throws-> Void) -完全创建自己的日期解码格式

解析嵌套对象

我们已经研究了如何在其他模型中使用数据模型,但是有时有必要在不使用单独数据模型的情况下解析其他字段中包含的JSON字段。 如果我们以一个例子来考虑,这个问题将更加清楚。 我们有以下JSON:

 { "id": 349, "art": "M0470500", "title": "- Vichy 50 ", "ratings": { "average_rating": 4.1034, "votes_count": 29 } } 


我们需要解析“ average”“ votes_count”字段 ,这可以通过两种方式解决:创建具有两个字段的Ratings数据模型并将数据保存到其中,或者可以使用nestedContainer 。 我们已经讨论了第一种情况,第二种情况的用法如下所示:

 class Product: Decodable { var id: Int var art: String? var title: String? var votesCount: Int var averageRating: Double enum CodingKeys: String, CodingKey { case id case art case title case ratings } enum RatingsCodingKeys: String, CodingKey { case votesCount = "votes_count" case averageRating = "average_rating" } required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.id = try container.decode(Int.self, forKey: .id) self.art = try? container.decode(String.self, forKey: .art) self.title = try? container.decode(String.self, forKey: .title) // Nested ratings let ratingsContainer = try container.nestedContainer(keyedBy: RatingsCodingKeys.self, forKey: .ratings) self.votesCount = try ratingsContainer.decode(Int.self, forKey: .votesCount) self.averageRating = try ratingsContainer.decode(Double.self, forKey: .averageRating) } } 


也就是说,通过使用nestedContainter创建另一个附加容器并对其进行进一步解析,可以解决此问题。 如果嵌套字段的数量不是很大,则此选项很方便,否则最好使用其他数据模型。

JSON字段名称和数据模型属性不匹配

如果您关注数据模型中枚举的定义方式,您会发现有时会为枚举的元素分配一个更改默认值的字符串,例如:

 enum RatingsCodingKeys: String, CodingKey { case votesCount = "votes_count" case averageRating = "average_rating" } 


这样做是为了正确匹配模型变量和JSON字段的名称。 对于名称由多个单词组成的字段,通常需要使用此字段,在JSON中,这些字段由下划线分隔。 原则上,这种重新定义枚举是最受欢迎的,而且看起来很简单,但是即使到那时,Apple还是提出了一个更优雅的解决方案。 可以使用keyDecodingStrategy一步解决此问题。 此功能出现在Swift 4.1中

假设您具有以下形式的JSON:

 let jsonString = """ [ { "name": "MacBook Pro", "screen_size": 15, "cpu_count": 4 }, { "name": "iMac Pro", "screen_size": 27, "cpu_count": 18 } ] """ let jsonData = Data(jsonString.utf8) 


让我们为其创建一个数据模型:

 struct Mac: Codable { var name: String var screenSize: Int var cpuCount: Int } 

模型中的变量根据协议进行记录,以小写字母开头,然后每个单词以大写字母开头(即所谓的camelCase )。 但是在JSON中,字段用下划线(所谓的snake_case编写 。 现在,为了使解析成功,我们需要在数据模型中定义一个枚举,在该枚举中,我们将建立JSON字段名称与变量名称的对应关系,否则将获得运行时错误。 但是现在可以简单地定义keyDecodingStrategy

 let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase do { let macs = try decoder.decode([Mac].self, from: jsonData) } catch { print(error.localizedDescription) } 


对于编码函数,您可以相应地使用逆变换:

 encoder.keyEncodingStrategy = .convertToSnakeCase 


也可以使用以下闭包来自定义keyDecodingStrategy

 let jsonDecoder = JSONDecoder() jsonDecoder.keyDecodingStrategy = .custom { keys -> CodingKey in let key = keys.last!.stringValue.split(separator: "-").joined() return PersonKey(stringValue: String(key))! } 


例如,该条目允许对JSON使用分隔符“-”。 使用的JSON示例:

 { "first-Name": "Taylor", "last-Name": "Swift", "age": 28 } 

因此,通常可以避免对枚举的附加定义。

错误处理

在解析JSON并将数据从一种格式转换为另一种格式时,错误是不可避免的,因此让我们看一下处理不同类型错误的选项。 解码时,可能发生以下类型的错误:

  • DecodingError.dataCorrupted(DecodingError.Context) -数据已损坏。 通常意味着您尝试解码的数据与预期格式不匹配,例如,您收到了完全不同的格式,而不是预期的JSON。
  • DecodingError.keyNotFound(CodingKey,DecodingError.Context) -找不到请求的字段。 表示您希望接收的字段丢失
  • DecodingError.typeMismatch(Any.Type,DecodingError.Context) -类型不匹配。 当模型中的数据类型与接收字段的类型不匹配时
  • DecodingError.valueNotFound(Any.Type,DecodingError.Context) -缺少特定字段的值。 您在数据模型中定义的字段无法初始化,可能在接收到的数据中此字段为nil。 仅在非可选字段中会发生此错误,如果该字段不必具有值,请不要忘记将其设置为可选。


编码数据时,可能会发生错误:

EncodingError.invalidValue(Any.Type,DecodingError.Context) -无法将数据模型转换为特定格式

解析JSON时的错误处理示例:

  do { let decoder = JSONDecoder() _ = try decoder.decode(businessReviewResponse.self, from: data) } catch DecodingError.dataCorrupted(let context) { print(DecodingError.dataCorrupted(context)) } catch DecodingError.keyNotFound(let key, let context) { print(DecodingError.keyNotFound(key,context)) } catch DecodingError.typeMismatch(let type, let context) { print(DecodingError.typeMismatch(type,context)) } catch DecodingError.valueNotFound(let value, let context) { print(DecodingError.valueNotFound(value,context)) } catch let error{ print(error) } 


错误处理当然最好放在单独的函数中,但是在这里,为清楚起见,错误分析与解析一起进行。 例如,如果“ product”字段没有值,则错误输出将如下所示:

图片

可编码和NSCoding的比较

当然,可编码协议在编码/解码数据方面迈出了一大步,但是NSCoding协议早在它之前就已经存在。 让我们尝试比较它们,看看Codable有什么好处:

  • 使用NSCoding协议时,对象必须是NSObject的子类,这自动暗示我们的数据模型应该是一个类。 在Codable中 ,分别不需要继承,数据模型可以是class,也可以是struct和enum
  • 如果您需要单独的编码和解码功能(例如,在解析通过API接收的JSON数据的情况下),则只能使用一种Decodable协议。 也就是说,无需实现有时不必要的初始化编码方法。
  • Codable可以自动生成所需的init和encoding方法,以及可选的CodingKeys枚举。 当然,这仅在数据结构中具有简单字段的情况下才有效,否则,将需要其他自定义。 在大多数情况下,尤其是对于基本数据结构,您可以使用自动生成,尤其是在重新定义keyDecodingStrategy的情况下 ,这很方便并且可以减少一些不必要的代码。


编码,可解码和可编码协议使我们向数据转换的便利性又迈出了一步,新的,更灵活的解析工具出现了,代码量减少了,并且部分转换过程实现了自动化。 协议是本机在Swift 4中实现的,因此可以在保持可用性的同时减少对第三方库(如SwiftyJSON )的使用。 协议还可以通过将数据模型和使用它们的方法分离到单独的模块中来正确组织代码结构。

Source: https://habr.com/ru/post/zh-CN414221/


All Articles