嵌套复杂的JSON
在实际应用中可能遇到JSON的嵌套结构,包含多个层级。例如:
{
"batchcomplete":"","query":{
"pages":{
"2887316":{
"pageid":2887316,"ns":0,"title":"Rizhao","index":-1,
"coordinates":[
{
"lat":35.417,"lon":119.527,"primary":"","globe":"earth"
}
],
"thumbnail":{
"source":"https://upload.wikimedia.org/wikipedia/commons/thumb/c/cd/Rizhao_sea-port.jpg/500px-Rizhao_sea-port.jpg","width":500,"height":333
},
"terms":{
"description":["prefecture-level city in Shandong, China"]
}
}
}
...
}
}
声明的结构体代码为:
struct WikiResponse: Codable {
let batchcomplete: String
let query: WikiQuery
}
struct WikiQuery: Codable {
let pages: [String: WikiPage]
}
struct WikiPage: Codable {
let pageid: Int
let ns: Int
let title: String
let index: Int
let coordinates: [WikiCoordinate]?
let thumbnail: WikiThumbnail?
let terms: [String: [String]]?
}
struct WikiCoordinate: Codable {
let lat: Double
let lon: Double
let primary: String?
let globe: String
}
struct WikiThumbnail: Codable {
let source: String
let width: Int
let height: Int
}
这个结构体是如何声明的呢?
第一步:观察JSON顶部结构
首先,我们先定义最外层的,我们可以根据 { } 来判断是否是对象,最外层是 { } ,这也就表示这是一个对象,需要声明一个对应的结构体。
// 最外层的结构体
{
"batchcomplete":"",
"query":{
// 另一个结构体
}
}
在这里,我声明的结构体名称为WikiResponse,并且包含batchcomplete和query这两个属性:
struct WikiResponse: Codable {
let batchcomplete: String
let query: WikiQuery
}
而query后面是 { } 包裹,所以这代表query的值是一个对象,所以需要创建新的结构体与之对应。
第二步:进入”query”字典内部
"query": {
"pages": {
"2887316": { ... }
}
}
这里的query就是一个对象,在顶部结构中将这一对象命名为WikiQuery。
struct WikiQuery: Codable {
let pages: WikiNum? // 这里的WikiNum应该如何定义?
}
这里存在的一个问题,那就是2887316这是一个数字,而不是具体的字段名称。
因为 JSON 允许对象的键是任意字符串,而这个结构设计就是为了以 pageid 作为键来组织数据。
换句话说,JSON 本身没有“数组中每项都长得一样”的强制性。在这个例子里,设计者把每个页面的 pageid(如 “2887316”)作为 pages 对象的键,而不是把它放到对象内部。这是一种「以 ID 为 key 的哈希表式结构」。
"pages": {
"2887316": { "pageid": 2887316, "title": "Rizhao", ... },
"123456": { "pageid": 123456, "title": "Beijing", ... }
}
这里的 “2887316” 和 “123456” 是对象的key,对应 Swift 中的 Dictionary<String, Page>。

因此,在Swift中就会被解析为字典类型:
struct WikiQuery: Codable {
let pages: [String: Page] // 字典 key 是 pageid 的字符串形式
}
在拿到数据后,可以通过for-in循环展示:
for (id, page) in response.query.pages {
print("PageID: \(id), Title: \(page.title)")
}
为什么不使用[Page]形式?
"pages": [
{ "pageid": 2887316, "title": "Rizhao" },
{ "pageid": 123456, "title": "Beijing" }
]
这种设计会更方便直接解析为 [Page](数组),对应的Swift解码结构可以为:
struct WikiQuery: Codable {
let pages: [WikiPage]
}
这种设计更适合顺序遍历、列表展示,Swift解码更方便。
但是,如果想要查找某个特定的pageid页面时,就需要遍历整个数组:
if let page = pages.first(where: { $0.pageid == 2887316 }) {
...
}
当页面比较多时,查找效率低。
而字典结构在Swift中可以快速按ID查找。
"pages": {
"2887316": { "pageid": 2887316, ... },
"1234567": { "pageid": 1234567, ... }
}
Swift结构为:
struct WikiQuery: Codable {
let pages: [String: WikiPage]
}
通过iD查找:
if let page = query.pages["2887316"] {
...
}
这是O(1)的查找,效率高。避免ID重复出现在每个对象中,减少数据体积。
缺点就是在使用ForEach(pages)遍历时不如数组方便,需要先用.values.sorted() 等方法转换:
let pageList = query.pages.values.sorted { $0.pageid < $1.pageid }
这个JSON的来源于Wikipedia,这类API通常面对的是“用户指定ID查找某些页面”,而不是展示所有的页面。
因此使用字典是为了支持高效按ID访问。
第三步:定义WikiPage
"2887316": {
"pageid": 2887316,
"ns": 0,
"title": "Rizhao",
"index": -1,
"coordinates": [ ... ],
"thumbnail": { ... },
"terms": {
"description": [ "..." ]
}
}
这一部分结构需要定义为 WikiPage:
struct WikiPage: Codable {
let pageid: Int
let ns: Int
let title: String
let index: Int
let coordinates: [WikiCoordinate]?
let thumbnail: WikiThumbnail?
let terms: [String: [String]]?
}
因为coordinates, thumbnail, terms 都可能不存在,所以需要加上可选标识 ?
第四步:定义 WikiCoordinate 和 WikiThumbnail
struct WikiCoordinate: Codable {
let lat: Double
let lon: Double
let primary: String?
let globe: String
}
struct WikiThumbnail: Codable {
let source: String
let width: Int
let height: Int
}
总结
面对嵌套复杂的JSON时,需要从顶部结构开始,逐渐声明结构体。
面对可能为空的字段,应该添加可选表示?,避免在解码的过程中报错。
在声明WikiPage结构体时,有人可能会将terms声明为一个结构体,而不是字典类型。
struct Terms: Codable {
let description: [String]
}
let terms: Terms?
这两种写法质上描述的是同一个 JSON 结构,但在 Swift 的 Codable 解码模型设计上,体现了两种建模方式的风格差异。
示例JSON:
"terms": {
"description": ["prefecture-level city in Shandong, China"]
}
写法1:扁平字典
let terms: [String: [String]]?
这里将JSON中的terms映射为一个字典类型,从而得到:
terms?["description"] // -> ["prefecture-level city in Shandong, China"]
这个写法的优点为不需要再定义新的结构体。
缺点就是不如结构体直观,不利于类型安全与代码提示。
写法2:结构体封装
struct Terms: Codable {
let description: [String]
}
let terms: Terms?
这种写法是将 terms 封装成结构体,只取出关心的字段 “description”。
可以通过 .first 等数组方法访问:
terms?.description.first
这个写法的优点是类型清晰,代码更可读、更安全,同时可以扩展结构体属性。
缺点就是如果terms字段结构不固定、字段很多时,每个字段都需要再结构中定义,增加了维护成本。
因此,如果字段固定,建议用结构体建模(更符合Swift风格)。
如果字段名可能变化,可以使用字典临时处理。
相关文章
1、Swift Codable的容错机制:解码不完全匹配的JSON:https://fangjunyu.com/2024/12/01/swift-codable%e7%9a%84%e5%ae%b9%e9%94%99%e6%9c%ba%e5%88%b6%ef%bc%9a%e8%a7%a3%e7%a0%81%e4%b8%8d%e5%ae%8c%e5%85%a8%e5%8c%b9%e9%85%8d%e7%9a%84json/
2、Swift深入理解Codable协议解码JSON数据:https://fangjunyu.com/2025/05/07/swift%e6%b7%b1%e5%85%a5%e7%90%86%e8%a7%a3codable%e5%8d%8f%e8%ae%ae%e8%a7%a3%e7%a0%81json%e6%95%b0%e6%8d%ae/
附录
完整的嵌套JSON代码
JSON内容:
{
"batchcomplete": "",
"query": {
"pages": {
"2887316": {
"pageid": 2887316,
"ns": 0,
"title": "Rizhao",
"index": -1,
"coordinates": [
{
"lat": 35.417,
"lon": 119.527,
"primary": "",
"globe": "earth"
}
],
"thumbnail": {
"source": "https://upload.wikimedia.org/wikipedia/commons/thumb/c/cd/Rizhao_sea-port.jpg/500px-Rizhao_sea-port.jpg",
"width": 500,
"height": 333
},
"terms": {
"description": [
"prefecture-level city in Shandong, China"
]
}
},
"24065443": {
"pageid": 24065443,
"ns": 0,
"title": "Donggang, Rizhao",
"index": 0,
"coordinates": [
{
"lat": 35.4254,
"lon": 119.4622,
"primary": "",
"globe": "earth"
}
],
"thumbnail": {
"source": "https://upload.wikimedia.org/wikipedia/commons/e/e9/ChinaRizhaoDonggang.png",
"width": 495,
"height": 540
},
"terms": {
"description": [
"district of China"
]
}
},
"41548663": {
"pageid": 41548663,
"ns": 0,
"title": "Port of Rizhao",
"index": 1,
"coordinates": [
{
"lat": 35.38333333,
"lon": 119.55833333,
"primary": "",
"globe": "earth"
}
],
"terms": {
"description": [
"Port in People's Republic of China"
]
}
},
"67247279": {
"pageid": 67247279,
"ns": 0,
"title": "Rizhao railway station",
"index": 2,
"coordinates": [
{
"lat": 35.403977,
"lon": 119.527174,
"primary": "",
"globe": "earth"
}
],
"thumbnail": {
"source": "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a9/Rizhao_Railway_station.jpg/500px-Rizhao_Railway_station.jpg",
"width": 500,
"height": 375
},
"terms": {
"description": [
"railway station in Rizhao, Shandong, China"
]
}
}
}
}
}