Swift中的JSON解析—第一部分:JSON解析的通用协议

对于iOS开发人员而言,解析JSON是一项非常常见的任务。 但是,Foundation框架提供的现成功能非常基础。 有许多可用的开源库可实现更高级别的功能,并有望使此任务更轻松,更安全。

作为个人实验,我尝试实现自己的JSON解析库,该库以我真正喜欢的几种方法为模型。 我的实现旨在实现极简主义,并专注于基于通用协议的方法,该方法应允许解析JSON内容,并以最少的代码将其存储在适当的容器( 结构实例)中。 为了使复杂性最小化,将没有特殊的错误处理:在不幸的情况下,解析特定的JSON密钥失败,相应的存储值将为nil

开箱即用的解决方案

假设我们需要与Web服务进行交互,该Web服务返回如下所示的JSON内容:

  { 
“位置”:[{
“ label”:“首页”,
“数据”:{
“ address”:“ 6925 Felicity Coves”,
“ city”:“ East Davin”,
“ state”:“华盛顿”,
“国家”:“美国”,
“ zipCode”:“ 22998-1456”
}
},
{
“ label”:“工作”,
“数据”:{
“ address”:“ 0506 Gretchen River”,
“ city”:“亨廷顿海滩”,
“ state”:“ Connecticut”,
“国家”:“美国”,
“ zipCode”:“ 61182-9561”
}
}]
}

Swift通过NSJSONSerialization类提供了一种解析JSON的默认方法。 我们可以将JSONObjectWithData用于此类任务,并在确认返回类型符合我们的预期( [String:AnyObject] )之后,我们能够将解析后的内容作为字典来访问:

  // JSON内容已作为NSData检索并存储在数据中 
做{
守护让json = try
NSJSONSerialization.JSONObjectWithData(data,
选项:.AllowFragments)为? [String:AnyObject]
其他{
返回零
}
  var标签= [String]() 
如果让locations = json [“ locations”]为? [[String:AnyObject]] {
用于位置{
如果让label = location [“ label”]为? 字符串{
labels.append(标签)
}
}
}
} {
//处理解析错误
}

此策略效果很好,但有点乏味。 特别是,每次从字典键中提取值时,都需要向下转换内容(作为可选),以指定期望的类型。

解析JSON的通用协议:JSONDecodable

无需像上面那样操作JSON内容,我将说明一种使用更简洁的语法执行相同任务的非常简单的方法。 提议的方法基于以下步骤:

  1. 定义一个协议,以轻松地将JSON内容解析为适当设计的容器( 结构实例)
  2. 定义将存储JSON数据的容器
  3. 定义JSON键和容器属性之间的映射
  4. 应用功能概念来简化解析语法

我的方法的基石将是通用协议 。 我不会详细介绍Swift中的通用协议以及可以用来解决什么类型的问题(也许在下一篇文章中……)。 我们只是说它提供了一种定义接受通用类型的协议的方法。 或如Russ Bishop所说的那样:

协议中的关联类型表示“我不知道这是什么确切类型; 一些采用我的具体类/结构/枚举将填写详细信息”。

拉斯·毕晓普

这是定义我们解析通用JSON内容[ .1 ]所需方法的协议:

 公共协议JSONDecodable { 
typealias DecodableType //(Swift 2.2)
// relatedtype DecodableType(Swift 2.3)

静态函数解码(json:JSON)-> DecodableType?
静态函数解码(json:JSON?)-> DecodableType?
静态函数解码(json:[JSON])-> [DecodableType?]
静态函数解码(json:[JSON]?)-> [DecodableType?]
}
 公共扩展JSONDecodable { 

静态函数解码(json:JSON?)-> DecodableType? {
守卫let json = json else {return nil}
返回解码(json)
}
 静态函数解码(json:[JSON])-> [DecodableType?] { 
返回json.map(decode)
}
 静态函数解码(json:[JSON]?)-> [DecodableType?] { 
守卫let json = json else {return []}
返回解码(json)
}
}

注意 从Swift 2.2开始,协议中的关联类型使用typealias声明。 在Swift 2.3中,不推荐使用此方法,而推荐使用associatedtype关键字。

上面的协议定义了一组方法,使我们能够轻松解析JSON内容。 请注意,每个解码方法都返回DecodableType类型的可选或可选数组。 这是因为解析可能会失败(由于错误,缺少值或键的拼写错误),并且我们希望针对该特定情况返回nilDecodableType标识JSONDecodable协议所需的通用类型( 关联的类型 )。 在编译时,它将由实现协议并提供JSON密钥和容器属性之间映射的容器的特定具体类型( struct )替换。

通过实现JSONDecodable协议中定义的四种协议方法,我们应该能够解析任何类型的JSON内容。 其中三个具有默认实现,作为JSONDecodable协议扩展提供。 这些方法处理标准的解析方案,并且最终将调用由符合JSONDecodable协议的容器提供的特定解码方法,以解析要存储在其属性中的内容:

 静态函数解码(json:JSON)-> DecodableType? 

此方法的实现取决于我们需要解析的特定JSON内容。 基本上,每个容器( struct )都必须描述如何通过提供其属性与包含所需数据的JSON对象的键之间的映射来从JSON内容检索其属性值。 反过来,这将使解析JSON内容成为定义一些适当的容器的问题,这些容器将1:1映射到JSON数据中表示的对象。 通过适当地实现解码方法(我们将在本文的第二部分中看到),我们将能够指示顶级容器开始解析过程,并确保每个嵌套容器将依次通过以下方式继续该过程:它的特定解码方法,直到我们解析了整个内容。

现在,我们已经定义了用于解析JSON内容的协议[ 1. ],让我们看一下下一步:定义将存储解析后的数据[ 2 ]的容器。

定义用于存储JSON内容的容器

此步骤取决于我们要解析的特定JSON内容,因为必须对容器进行适当的建模。 记住本文开头显示的JSON示例,让我们来看一下正确解析容器所需的容器定义:

 结构位置{ 
让标签:字符串?
让数据:LocationData?
  init(label:String ?, 
数据:LocationData?)
{
self.label =标签
self.data =数据
}
}