在 Swift 中,Codable 类型在解码 JSON 时具有很强的容错能力。当 JSON 数据的结构和 Codable 定义的模型不完全匹配时,只要关键字段和数据类型能够对上,JSONDecoder 仍然会尽量解析它,忽略多余或不匹配的部分。
参考样例
例如,通过维基百科获取相关日照坐标的相关信息:
@State private var pages = [Page]()
func fetchNearbyPlaces() async {
let urlString = "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"
do {
let (data, _) = try await URLSession.shared.data(from: URL(string: urlString)!)
let items = try JSONDecoder().decode(Result.self, from: data)
pages = items.query.pages.values.sorted { $0.title < $1.title }
} catch {
print("调取失败")
}
}
✓
当调取成功后,在视图中展示:
ForEach(pages, id: \.pageid) { page in
Text(page.title)
.font(.headline)
+ Text(": ") +
Text("Page description here")
.italic()
}
✓

JSON结构
JSON文件内容
通过URLSession.shared.data获取的实际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"]
}
}
}
...
}
}
✓
结构分解
1、Result结构:batchcomplete:String,query:Query
2、Query结构:pages:String[Int,PageInfo]
3、PageInfo结构:pageid:Int,ns:Int,title:String,index:Int,coordinates:[Coordinates],thumbnail:Thumbnail,terms:Terms
4、Coordinates结构:lat:Double,lon:Double,primary:String,globe:String
5、Thumbnail结构:source:String,width:Int,height:Int
6、Terms结构:description:[String]
Swift中的结构
struct Result: Codable {
let query: Query
}
struct Query: Codable {
let pages: [Int: Page]
}
struct Page: Codable {
let pageid: Int
let title: String
let terms: [String: [String]]?
}
✓
对比发现,JSON文件的结构与Swift中定义的结构并不一致,但仍然可以解码出数据信息。
Codable容错机制
1、忽略JSON 多余字段
JSON 中可能包含模型未定义的字段,这些字段会被解码器自动忽略。例如:
{
"query": {
"pages": {
"12345": {
"pageid": 12345,
"title": "Example Title",
"terms": {
"description": ["An example page"]
},
"extra_field": "This is ignored"
}
}
}
}
✓
即使 Page 中没有 extra_field 属性,解码器也不会报错,只会忽略 extra_field。
2、可选字段为nil
如果 JSON 数据中没有 terms 字段,解码器会将其赋值为 nil,不会引发错误。例如:
{
"query": {
"pages": {
"12345": {
"pageid": 12345,
"title": "Example Title"
}
}
}
}
✓
在这种情况下,terms 被安全地赋值为 nil。
3、字典键类型的动态处理
Query 中 pages 定义为 [Int: Page],但 JSON 中的键是字符串,例如 “12345”。解码器会尝试将键从字符串转换为整数,只要转换成功,就不会报错。如果转换失败,则会抛出错误。例如:
{
"query": {
"pages": {
"invalid_key": {
"pageid": 12345,
"title": "Example Title"
}
}
}
}
✓
这里 “invalid_key” 无法转换为 Int,解码会失败。
4、容忍 JSON 结构
Codable 的解析是基于字段匹配的,它不要求 JSON 数据完全按照模型定义。只要 JSON 中包含模型需要的字段,并且字段类型兼容,解码就能成功。
为什么能解码?
综上所述,只要:
1、模型定义的字段存在于 JSON 中,且类型匹配;
2、未定义的字段被忽略;
3、可选字段可以缺失;
解码器就能正确解析 JSON。
总结
Swift Codable容错机制,有助于更好的解析JSON的字段,当想要存储指定字段时,可以在Swift结构中调整对应的字段。
需要注意的是,Swift 的 Decodable 在解析 JSON 时默认要求每个键都必须存在。如果某个键缺失(例如 thumbnail),解码就会失败,抛出此错误。
因此,应该对可能缺失的字段设置为可选类型:
struct PageInfo: Codable {
let pageid: Int
let ns: Int
let title: String
let index: Int
let coordinates: [Coordinates]
let thumbnail: Thumbnail?
let terms: Terms?
}
✓
也可以考虑在构造器中配置默认值:
struct Page: Codable {
let pageid: Int
let title: String
let thumbnail: Thumbnail
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
pageid = try container.decode(Int.self, forKey: .pageid)
title = try container.decode(String.self, forKey: .title)
// Provide a default value if thumbnail is missing
thumbnail = (try? container.decode(Thumbnail.self, forKey: .thumbnail)) ?? Thumbnail.default
}
private enum CodingKeys: String, CodingKey {
case pageid, title, thumbnail
}
}
✓
如果解码的过程中,存在无法捕获的问题,可以使用DecodingError捕获问题字段。
附录
JSON数据结构的字典类型
前文中的JSON文件的pages属性中, “2887316”数字并没有属性名称:
{
"batchcomplete":"","query":{
"pages":{
"2887316":{
...
}
}
}
}
✓
这是因为,在 JSON 数据结构中,”pages” 是一个字典(或对象),其键是动态的字符串(在这里是 2887316),而不是固定的键名。这种结构在某些情况下非常常见,尤其是在需要按唯一标识符(如 id)组织数据的场景。
解析这种结构的特点
1、动态键值对:
在 JSON 中,键并不需要是固定的名字。这里的 2887316 作为键,代表了某种唯一标识符,例如页面 ID。
2、键作为动态标识符:
由于不同的数据条目可能具有不同的 ID,JSON 使用动态的键来表示这些条目。这样可以直接通过键来索引特定的数据,而不是在每个条目中显式存储一个 id 字段。
3、字典类型的用途:
这种结构实际上是一种字典(在 Swift 中对应 Dictionary<String, Value>)。动态的键名可以高效地存储和查找数据,尤其适用于数据量大且需要频繁查找的情况。
在 Swift 中,JSON 里的动态键可以用 Dictionary 类型来解析。例如,在示例代码中:
struct Query: Codable {
let pages: [Int: Page]
}
✓
这里的 pages 是 [Int: Page] 类型,Swift 会尝试将 JSON 的键转换为 Int 类型(因为这里的键是 “2887316”,可以被解析为整数)。这是 Codable 的一个强大功能,它能自动处理 JSON 中的动态键,只要键的类型和 JSON 的实际键兼容。
优点
节省冗余字段:不需要在每个条目中重复 id 字段,键本身就充当了唯一标识符。
快速查找:如果在解析后的数据中需要查找特定 ID 的条目,可以直接通过键访问,而不需要遍历整个数组。
Result.swift代码
struct Result: Codable {
let query: Query
}
struct Query: Codable {
let pages: [Int: Page]
}
struct Page: Codable {
let pageid: Int
let title: String
let terms: [String: [String]]?
}
✓
EditView.swift代码
import Foundation
import SwiftUI
struct EditView: View {
@Environment(\.dismiss) var dismiss
var location: Location
var onSave: (Location) -> Void
@State private var name: String
@State private var description: String
@State private var loadingState = LoadingState.loading
@State private var pages = [Page]()
init(location: Location,onSave: @escaping (Location) -> Void) {
self.location = location
self.onSave = onSave
_name = State(initialValue: location.name)
_description = State(initialValue: location.description)
}
func fetchNearbyPlaces() async {
let urlString = "https://en.wikipedia.org/w/api.php?ggscoord=\(location.latitude)%7C\(location.longitude)&action=query&prop=coordinates%7Cpageimages%7Cpageterms&colimit=50&piprop=thumbnail&pithumbsize=500&pilimit=50&wbptterms=description&generator=geosearch&ggsradius=10000&ggslimit=50&format=json"
guard let url = URL(string: urlString) else {
print("Bad URL: \(urlString)")
return
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
// we got some data back!
let items = try JSONDecoder().decode(Result.self, from: data)
// success – convert the array values to our pages array
pages = items.query.pages.values.sorted { $0.title < $1.title }
loadingState = .loaded
} catch {
// if we're still here it means the request failed somehow
loadingState = .failed
}
}
var body: some View {
NavigationStack {
Form {
Section {
TextField("Place name", text: $name)
TextField("Description", text: $description)
}
Section("Nearby…") {
switch loadingState {
case .loaded:
ForEach(pages, id: \.pageid) { page in
Text(page.title)
.font(.headline)
+ Text(": ") +
Text("Page description here")
.italic()
}
case .loading:
Text("Loading…")
case .failed:
Text("Please try again later.")
}
}
}
.navigationTitle("Place details")
.toolbar {
Button("Save") {
var newLocation = location
newLocation.id = UUID()
newLocation.name = name
newLocation.description = description
onSave(newLocation)
dismiss()
}
}
}
.task {
await fetchNearbyPlaces()
}
}
}
#Preview {
EditView(location: .example) { _ in }
}
enum LoadingState {
case loading, loaded, failed
}
✓
Result.swift代码(完整结构)
struct Result: Codable {
let batchcomplete: String
let query: Query
}
struct Query: Codable {
let pages: [Int:PageInfo]
}
struct PageInfo: Codable {
let pageid: Int
let ns: Int
let title: String
let index: Int
let coordinates: [Coordinates]
let thumbnail: Thumbnail?
let terms: Terms?
}
struct Coordinates: Codable {
let lat: Double
let lon: Double
let primary: String
let globe: String
}
struct Thumbnail: Codable {
let source: String
let width: Int
let height: Int
}
struct Terms: Codable {
let description: [String]
}
✓
EditView.swift代码(完整结构)
import Foundation
import SwiftUI
struct EditView: View {
@Environment(\.dismiss) var dismiss
var location: Location
var onSave: (Location) -> Void
@State private var name: String
@State private var description: String
@State private var loadingState = LoadingState.loading
@State private var pages = [PageInfo]()
init(location: Location,onSave: @escaping (Location) -> Void) {
self.location = location
self.onSave = onSave
_name = State(initialValue: location.name)
_description = State(initialValue: location.description)
}
func fetchNearbyPlaces() async {
let urlString = "https://en.wikipedia.org/w/api.php?ggscoord=\(location.latitude)%7C\(location.longitude)&action=query&prop=coordinates%7Cpageimages%7Cpageterms&colimit=50&piprop=thumbnail&pithumbsize=500&pilimit=50&wbptterms=description&generator=geosearch&ggsradius=10000&ggslimit=50&format=json"
guard let url = URL(string: urlString) else {
print("Bad URL: \(urlString)")
return
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
// we got some data back!
let items = try JSONDecoder().decode(Result.self, from: data)
// success – convert the array values to our pages array
pages = items.query.pages.values.sorted { $0.title < $1.title }
loadingState = .loaded
} catch DecodingError.keyNotFound(let key, let context) {
print("---1---")
print("Key '\(key)' not found:", context.debugDescription)
print("codingPath:", context.codingPath)
loadingState = .failed
} catch DecodingError.typeMismatch(let type, let context) {
print("---2---")
print("Type '\(type)' mismatch:", context.debugDescription)
print("codingPath:", context.codingPath)
loadingState = .failed
} catch DecodingError.valueNotFound(let type, let context) {
print("---3---")
print("Value '\(type)' not found:", context.debugDescription)
print("codingPath:", context.codingPath)
loadingState = .failed
} catch DecodingError.dataCorrupted(let context) {
print("---4---")
print("Data corrupted:", context.debugDescription)
print("codingPath:", context.codingPath)
loadingState = .failed
} catch {
print("Unexpected error: \(error)")
loadingState = .failed
}
}
var body: some View {
NavigationStack {
Form {
Section {
TextField("Place name", text: $name)
TextField("Description", text: $description)
}
Section("Nearby…") {
switch loadingState {
case .loaded:
ForEach(pages, id: \.pageid) { page in
Text(page.title)
.font(.headline)
+ Text(": ") +
Text("Page description here")
.italic()
}
case .loading:
Text("Loading…")
case .failed:
Text("Please try again later.")
}
}
}
.navigationTitle("Place details")
.toolbar {
Button("Save") {
var newLocation = location
newLocation.id = UUID()
newLocation.name = name
newLocation.description = description
onSave(newLocation)
dismiss()
}
}
}
.task {
await fetchNearbyPlaces()
}
}
}
#Preview {
EditView(location: .example) { _ in }
}
enum LoadingState {
case loading, loaded, failed
}
✓