Swift构建嵌套复杂的JSON的Codable模型结构
Swift构建嵌套复杂的JSON的Codable模型结构

Swift构建嵌套复杂的JSON的Codable模型结构

嵌套复杂的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地址:https://en.wikipedia.org/w/api.php?ggscoord=35.420503%7C119.527506&action=query&prop=coordinates%7Cpageimages%7Cpageterms&colimit=50&piprop=thumbnail&pithumbsize=500&pilimit=50&wbptterms=description&generator=geosearch&ggsradius=10000&ggslimit=50&format=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"
                    ]
                }
            }
        }
    }
}
   

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开发者。

欢迎加入我们的 微信交流群QQ交流群,交流更多精彩内容!
微信交流群二维码 QQ交流群二维码

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注