可编码—改进解码JSON的4种方法

Swift 4中引入的Codable提供了一种方便,轻松的方式来编码和解码JSON。 但是,并不是所有的东西都开箱即用。

Swift 4中引入的Codable提供了一种方便,轻松的方式来编码和解码JSON。 但是,并不是所有的东西都开箱即用。 例如,让我们看一下Twitter中的JSON文件。 作为JSON解码器工作方式的结果,我们看到:

  • 引用和标准推文使用相似但不完全相同的字段。
  • 转换为属性名称的键不遵循Swift约定。
  • 日期不是采用标准的接受格式。
  • 颜色的格式不受任何Color类 (UIColor,NSColor等)接受。

因此,我们将学习如何为某些特殊情况设置您的Codable类型。 具体来说,我们将研究:

  • 对可编码类型使用协议
  • 不同的属性名称和键
  • 属性值中的日期
  • 自定义类型的属性值

要继续学习,您可以在GitHub上查看本文的Xcode游乐场。

JSONDecoder的工作方式

这些是JSONDecoder如何将JSON转换为结构化类型的基本规则:

  1. 属性名称按原样转换。
  2. 默认情况下,简单属性值将转换为StringIntDouble
  3. 遵循正确格式的属性值可以解码为URLDataDate
  4. 任何属性值都可以认为是Optional
  5. 方括号[]值将转换为Array
  6. 花括号{}值将转换为Dictionary或自定义类型。
  7. 所有需要解码的类型都需要实现Decodable协议。 同样,需要编码的类型需要实现可编码协议。 如果需要同时对类型进行解码和编码,则可以简单地实现Codable。

在这种情况下,我们将查看为一条推文返回的Twitter JSON数据。 因此, 我们要做的第一件事就是为每种返回的数据类型创建一些基本结构 。 结果,您可以在仓库的此分支上查看解码工作的第一步。

即使数据相似,我们也会创建单独的结构以提供更大的灵活性。 但是,这并不意味着我们不能使用协议来简化功能中这些类型的使用。

将协议与可编码一起使用

例如,在Twitter API中,始终使用tweet的概念。 举例来说,他们的JSON有一条主推文和一条引号 。 因此,可能倾向于对两者使用相同的结构。 但是,更好的方法是两个单独的结构:

  public struct Tweet:可编码{ 
public let created_at:日期
公开让ID:Int
public let full_text:字符串
public let display_text_range:[Int]
公共租赁实体:TweetEntities
public let source:字符串
public let in_reply_to_status_id:整数?
public let in_reply_to_user_id:整数?
public let in_reply_to_screen_name:字符串
公开让用户:TweetUser
公开让quoted_status:QuotedTweet吗?
公开让is_quote_status:布尔
public let retweet_count:整数
public let favorite_count:整数
公众喜欢:布尔
公开让转推了:布尔
公开让可能_敏感:布尔
公开let lets_sensitive_appealable:布尔
public let lang:字符串
}
 公共结构QuotedTweet:可编码{ 
public let created_at:日期
公开让ID:
public let full_text:字符串
public let display_text_range:[Int]
公共租赁实体:TweetEntities
公开让用户:TweetUser
public let source:字符串
public letextended_entities:TweetEntities
公开让is_quote_status:布尔
public let retweet_count:整数
public let favorite_count:整数
公众喜欢:布尔
公开让转推了:布尔
公开让可能_敏感:布尔
公开let lets_sensitive_appealable:布尔
public let lang:字符串
}

因此,这意味着重复的字段。 但是,它也允许更好的灵活性和更容易的JSON解码 。 另一方面,当在函数中用作参数时,我们可以简化这些类型。 例如,假设我们需要打印推文:

  func printTweet(_ tweet:Tweet){ 
打印(tweet.full_text)
如果让quoted_status = tweet.quoted_status {
printTweet(quoted_status)
}
}
  func printTweet(_ tweet:QuotedTweet){ 
打印(“>”,tweet.full_text)
}

但是,使用一些基本的面向协议的编程 ,我们实际上可以对此进行优化。 首先,我们对一个函数进行存根,该函数根据协议打印出该推文,而不管它是否是引用的推文

  func printTweet(_ tweet:TweetProtocol,withQuoteLevel级别:Int = 0){ 
print(String(repeating:“>”,count:level),tweet.full_text)
如果让quotedTweet = tweet.quotedTweet {
printTweet(quotedTweet,withQuoteLevel:level + 1)
}
}

因此,我们创建了具有上面功能所需属性的协议。

 公共协议TweetProtocol { 
var full_text:字符串{get}
var quotedTweet:TweetProtocol? {得到}
}

最后,我们为将用于TweetQuotedTweet的两种类型实现协议:

 扩展名Tweet:TweetProtocol { 
public var quotedTweet:Tweet协议? {
返回self.quoted_status
}
}
 扩展QuotedTweet:TweetProtocol { 
public var quotedTweet:Tweet协议? {
返回零
}
}

现在, 我们可以更好地与Codable一起使用,同时使用Protocols添加类似的功能。 接下来,让我们清理属性名称。

自定义属性名称

关于Twitter的JSON,您注意到的一件事是, 它们使用所谓的 蛇形大写字母 来组合属性名称中的单词。 结果,下划线 _ 用于组合JSON密钥中的单词。 例如,一条推文全文的属性称为full_text 。 但是, Swift API设计指南 建议使用 驼峰式大小写。 骆驼案将每个新词大写。 因此,在这种情况下,我们的属性名称将为fullText 。 因此,我们有两种解决方法: 自定义CodingKeys或使用KeyEncodingStrategy。

自定义CodingKeys通过允许我们提供从JSON键到属性名的映射,为我们提供了最大的灵活性。 但是,在这种情况下, 存在将所有键映射到属性名称的一致策略。 因此,在这种情况下,我们将使用KeyDecodingStrategy特别是KeyDecodingStrategy.convertFromSnakeCase

 let decoder = JSONDecoder() 
decoder.keyDecodingStrategy = .convertFromSnakeCase

另外,如果KeyDecodingStrategy.convertFromSnakeCase不太适合您的JSON密钥,则Swift会允许使用自定义策略。 换句话说,您可以提供用于将CodingKey数组转换为单个CodingKey结果的闭包。
最后但并非最不重要的一点是,如果这些策略都不能将密钥转换为属性名称,那么您始终可以提供自定义的CodingKeys枚举。 简而言之,您想使用最简单,最一致的策略来映射属性名称或JSON键。

自定义属性值

Twitter的JSON数据的另一个怪癖是它们如何格式化某些属性值。 让我们看一下最容易处理的日期。

如何处理日期

默认情况下,日期从2001年1月1 日起被序列化为浮动小数,特别是TimeInterval(秒数)。但是,如果JSON属性值的格式不同,则可以使用以下可用策略之一:

因此, 如果日期格式不遵循上述任何一种格式则需要提供闭包来进行转换,或者提供 DateFormatter 在DateFormatter中指定格式的主要方法是使用dateFormat String属性。 换句话说,通过使用Unicode提供的代码,我们可以创建dateFormat字符串来表示Twitter提供的日期和时间的格式。 结果,从JSON中的日期Mon Mar 28 14:39:13 +0000 2016 ,我们可以将格式推断为: eee MMM dd HH:mm:ss ZZZZ yyyy 。 因此,我们可以创建一个DateFormatter并相应地设置策略:

 let dateFormat = "eee MMM dd HH:mm:ss ZZZZ yyyy" 
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = dateFormat
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(dateFormatter)
decoder.keyDecodingStrategy = .convertFromSnakeCase
return decoder

总之,通过自定义DateFormatter,可以将日期解码为Date属性值。 但是,颜色将更具挑战性。

属性值的自定义解码

对于配置文件背景色之类的颜色,我们需要创建一个单独的结构并自己实现编码和解码,以将十六进制字符串转换为可用的颜色结构:

 进口基金会 
导入CoreGraphics
 公共结构颜色:可编码{ 
公众让红色:CGFloat
公众让绿:CGFloat
公众让蓝色:CGFloat
公开租赁alpha:CGFloat
  public init(来自解码器:Decoder)抛出{ 
让hexCode =尝试解码器.singleValueContainer()。decode(String.self)
让扫描仪=扫描仪(string:hexCode)
var hexint:UInt32 = 0
扫描仪.scanHexInt32(&hexint)
  self.red = CGFloat((hexint&0xff0000)>> 16)/ 255.0 
self.green = CGFloat((hexint&0xff00)>> 8)/ 255.0
self.blue = CGFloat((hexint&0xff)>> 0)/ 255.0
self.alpha = 1
}
 公共功能编码(对编码器:编码器)抛出{ 
let string = String(格式:“%02lX%02lX%02lX”,lroundf(Float(红色* 255.0)),lroundf(Float(绿色* 255.0)),lroundf(Float(蓝色* 255.0)))
var容器= encoder.singleValueContainer()
尝试container.encode(string)
}
}

在这种情况下,我们将覆盖init方法,并从singleValueContainer获取字符串。 之后,我们使用扫描仪将彩色十六进制代码转换为十六进制数字。 然后使用按位和逻辑运算的组合来获取介于0到255之间的每个分量值。最后,我们使用十进制除法来计算介于0到1之间的值。(为完成起见,我也包括了encoding方法。)结果,我们可以轻松地将十六进制代码字符串转换为Color结构。

由于我们无法使用现有的系统颜色类型,因此以下代码可将我们的自定义Color结构转换为系统默认的UI颜色类型:

  #if os(iOS)|| 操作系统(watchOS)|| 操作系统(tvOS) 
导入UIKit
公共类型别名SystemColor = UIColor
#elseif os(macOS)
公共类型别名SystemColor = NSColor
#万一
 扩展名颜色{ 
public var systemColor:SystemColor {
返回SystemColor(红色:self.red,绿色:self.green,蓝色:self.blue,alpha:self.alpha)
}
}

使我们可以编码

可编码提供了许多可定制性。 但是,在更多情况下,我们希望使用最少的定制量和最多的一致性。

  • 对于以类似方式使用的数据,请使用单独的类型,但要实现相同的协议
  • 如果属性名称具有一致的策略,请使用KeyEncodingStrategy而不是自定义CodingKeys
  • 对于日期,请使用dateDecodingStrategy,DateFormatter或闭包
  • 对于其他自定义类型,请创建一个单独的类型,然后在此处自定义解码 (和编码)。

如果您还有其他问题要与我联系,请通过Twitter与我联系。 此外,如果您有兴趣了解有关复杂JSON解码或编码以及其他Swift主题的更多信息,请订阅我的时事通讯以保持最新。