Swift深入理解Codable协议解码JSON数据
Swift深入理解Codable协议解码JSON数据

Swift深入理解Codable协议解码JSON数据

对于SwiftUI解码JSON数据,可以参考《Swift UI解构JSON文件》一文,本文主要内容为:深入理解如何设置Swift结构以及整个解码流程

JSON数组结构

Swift可以识别JSON,因为它遵循了一种标准格式——JSON数组(Array of Objects),且语法完全符合 JSON 规范。

什么是JSON数组?

假设我们需要从货币网址获取到各种外币的最新币值,获取到的JSON格式为:

[
  {
    "id": "bitcoin",
    "symbol": "btc",
    "name": "Bitcoin",
    "image": "https://assets.coingecko.com/coins/images/1/large/bitcoin.png",
    "current_price": 95936.60,
    "market_cap": 1904967939513,
    "market_cap_rank": 1,
    "total_volume": 14351955699,
    "price_change_percentage_24h": 0.7,
    "last_updated": "2025-05-03T19:00:00.000Z"
  },
  {
    "id": "ethereum",
    "symbol": "eth",
    "name": "Ethereum",
    "image": "https://assets.coingecko.com/coins/images/279/large/ethereum.png",
    "current_price": 1834.30,
    "market_cap": 221406884198,
    "market_cap_rank": 2,
    "total_volume": 6985564402,
    "price_change_percentage_24h": 0.0,
    "last_updated": "2025-05-03T19:00:00.000Z"
  }
  // ... 其他币种
]

实际上这个JSON的结构为:

[
  { ... },  // 第一个币种
  { ... }   // 第二个币种
]

每个对象内部是一个标准的Key-Value字典结构(JSON Object),字段类型可以是字符串、数字、布尔、嵌套对象、数组等。

最外层的 [ ] 方括号就是数组,这就表示这是一个数组对象。

Swift结构

当我们从Bundle或者从网络上使用dataTask等方法获取API的JSON响应后,Swift通过Codable和JSONDecoder会自动识别并解析这种结构。

例如,在这个JSON中有多个对象。

Swift在解码JSON时,必须创建对应的模型(Model Struct),相关知识可以参考《Swift UI解构JSON文件》。

为什么需要创建对应的模型?

Swift 是一个强类型语言,它要求每个变量和属性都有明确的数据类型。

当使用 Codable 解码 JSON 时,Swift 需要知道:

1)每个字段的名称是什么(key)

2)每个字段的值类型是什么(如 String、Double、Date 等)

因此,创建结构体就是为了让编译器知道“应该解成什么样的对象”。

声明结构体

需要根据对象的字段,创建遵循Codable协议的结构体。

我们可以根据返回的JSON字符串字段的内容,映射成对应的类型。

其中,id、symbol和name都是””双引号包裹的字段,所以它们是String类型。

Image字段返回的是一个照片的链接,所以是URL类型。

current_price字段是一串数字,因为存在小数位并且没有双引号包裹,所以是Double类型。

market_cap、market_cap_rank和total_volume也是数字,但是没有小数位且没有双引号包裹,所以是Int类型。

最后的last_updated虽然是双引号包裹,但实际上是ISO 8601标准格式日期,所以是Date类型。

在映射字段时,可以根据是否有双引号包裹来判断是String类型还是数字类型。如果是数字类型,有小数位的是Double,没有小数位的是Int。双引号包裹内容,大部分是String类型,但URL和Date等类型也是通过双引号包裹,所以在细分双引号包裹内容时,需要检查内容是否为其他类型。

最后,根据JSON字符串对应的字段,可以创建出一个结构体,以符合编译器自动合成解码的逻辑。

struct Cryptocurrency: Codable {
    let id: String
    let symbol: String
    let name: String
    let image: URL
    let current_price: Double
    let market_cap: Int
    let market_cap_rank: Int
    let total_volume: Int
    let price_change_percentage_24h: Double
    let last_updated: Date
}

必须使用结构体么?

答案是否定的,可以选择Any 或 NSDictionary,但这样会失去类型信息,变得非常不安全:

let json = try JSONSerialization.jsonObject(with: data, options: []) as? [[String: Any]]

这种方式:

1)没有类型提示

2)访问字段时需要手动判断类型和强制转换

3)容易出错且不利于维护

目前还没有写一个详尽说明这种方法的文章,但可以参考类似的文章《Swift JSON和对象的转换类JSONSerialization

优化结构体

回到前面定义的结构体代码,在Swift中,属性名必须是合法的标识符,虽然语法允许使用下划线(_),但是Swift的命名风格是camelCase(推荐写法)。

1)可读性更强

2)与系统 API 保持一致

3)与 SwiftUI、Foundation 等库风格统一

比如 Swift 标准库也是:

struct DateFormatter {
    var dateFormat: String
}

而不是 date_format。

因此,我们应该考虑将属性名的命名风格改为camelCase。

但是,JSON数据是来自API,字段有可能是snake_case 格式。

{
  "current_price": 1834.3,
  "market_cap": 1234567890
}

这里的字段格式和Swift 的 camelCase 不一致。

这里就需要使用CodingKeys映射字段:

enum CodingKeys: String, CodingKey {
    case currentPrice = "current_price"
    case marketCap = "market_cap"
}

通过使用CodingKeys,Swift就知道JSON的”current_price” 应该对应 Swift 的 currentPrice,在编码(encode)和解码(decode)时自动完成字段映射。相关知识可以查看《Swift为什么定义CodingKeys》或者《Swift Codable 深入解析:理解 CodingKeys 的关键角色

最后,Codable模型代码为:

struct CryptoDTO: Codable {
    let id: String
    let symbol: String
    let name: String
    let image: URL
    let currentPrice: Double
    let marketCap: Int
    let marketCapRank: Int
    let totalVolume: Int
    let priceChangePercentage24h: Double
    let lastUpdated: Date

    enum CodingKeys: String, CodingKey {
        case id, symbol, name, image
        case currentPrice = "current_price"
        case marketCap = "market_cap"
        case marketCapRank = "market_cap_rank"
        case totalVolume = "total_volume"
        case priceChangePercentage24h = "price_change_percentage_24h"
        case lastUpdated = "last_updated"
    }
}

有人可能疑问为什么不能使用snake_case格式,而是需要借助CodingKeys遵守Swift的属性命名风格?

这是因为snake_case不符合Swift命名规范,Swift中会使用 _ 作为忽略参数或变量,例如 for _ in 0..<3,容易产生混淆。其次是,使用的过程中容易出错和不清晰,比如:

crypto.current_price  // 看起来像 C 语言,而非现代 Swift

还有一种方法可以自动映射索引的snake_case

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

Swift会自动把current_price映射为currentPrice。这样就可以省去CodingKeys。

需要注意的是,这种自动映射索引的方法要求属性名和转换后的字段严格匹配!

解码JSON数据

最后是通过JSONDecoder对JSON数据进行解码。JSONDecoder利用Codable协议的自动合成功能,将JSON数据映射成Swift中的结构体(如CryptoDTO)。

JSONDecoder是Swift专门用来将JSON数据解码为符合Decodable协议的Swift类型的工具,如需进一步了解JSONDecoder可以查看《Swift JSONDecoder类》。

首先,需要获取到JSON文件,通常有两种获取途径,一种是从Bundle中获取,另一种是使用URLSession.shared.dataTask从网络中获取,这两种获取途径的解析方法请见《Swift UI解构JSON文件》。

我这里是在Xcode的Playground中获取Bundle文件的URL:

let url = URL(fileURLWithPath: #file)
    .deletingLastPathComponent()
    .appendingPathComponent("coins.json")

接着,使用Data(contentsOf:)方法,通过URL读取Data数据。

guard let data = try? Data(contentsOf: url) else {
  print("data读取失败")
  return []
}

为什么使用Data(contentsOf:)方法呢?而不是其他的方法。这是因为后面用到的JSONDecoder.decode(_:from)方法要求二进制数据(Data),而不是纯文本(String)。如果使用String(contentsOf:)读取JSON数据,那么读取的就是文本内容而不是Data数据。

接着,声明一个JSONDecoder:

let decoder = JSONDecoder()

因为JSON数据存在ISO8601格式的日期,还需要给JSONDecoder配置dateDecodingStrategy日期格式,配置原因请见《Swift使用JSONDecoder解码日期之ISO 8601 标准格式》文章中涉及毫秒的部分。

let decoder = JSONDecoder()
        
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
formatter.locale = Locale(identifier: "en_US_POSIX")
decoder.dateDecodingStrategy = .formatted(formatter)

最后是使用JSONDecoder解码JSON数据,前面已经通过Data(contentsOf:)方法读取到了Data数据:

do {
  let cryptos = try decoder.decode([CryptoDTO].self, from: data)
  for crypto in cryptos {
      print("Name: \(crypto.name), Price: \(crypto.currentPrice)")
  }
} catch {
  print("JSONDecoder解码失败")
}

这里的核心用法就是JSONDecoder().decode()方法。这个方法可以将JSON数据(Data类型)转换成符合Decodable协议的Swift类型(如struct或class)。

func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable

decode一共需要两个参数:

第一个是类型本身本身T.self,例如 [CryptoDTO].Type 表示 [CryptoDTO]类型的元类型(metatype)。这是为了告诉decode()方法,要求将JSON解码为[CryptoDTO]类型的实例。

第二个参数是JSON的Data数据,也就是前面从Bundle中读取的JSON数据。

最后解码成功,并输出每一个对象的name和currentPrice属性:

Name: Sei, Price: 0.196895
Name: Story, Price: 3.71
...

解码成功后,我们就可以使用这个对象展示或存储到相应的位置。

如果解码存在报错,我们可以使用DecodingError获取到报错的字段,DecodingError的相关知识请见《Swift UI 深入理解 DecodingError》。

总结

以上就是使用Codable协议解码JSON文件的全部知识点。主要的难点在于将如何根据JSON文件声明结构体。当我们拥有结构体后,就可以借助JSONDecoder.decode()方法将数据解码出来。

涉及的文章知识点比较多,如果有不了解的地方,建议进一步阅读扩展文章。

扩展知识

嵌套JSON结构

请移步《Swift构建嵌套复杂的JSON的Codable模型结构》一文。

常见的Swift类型和JSON值

在声明结构体时,我们根据双引号判断是否为字符串、URL和Int等类型。

下面是常见的Swift类型和JSON值:

1、String:”abc”,文本内容。

2、Int:123,整数值(注意不能是浮点)。

3、Double:123.45,浮点数(JSON 没有区分 float 和 double)。

4、Bool:true / false,布尔值。

5、URL:https://…,自动识别成链接类型。

7、Date:”2025-01-01T12:00:00Z”,时间戳字符串(需要设置 decoder 策略)。

8、Optional<T>:缺失字段或 null,某个字段可能为空。

9、[T]:JSON 数组,例如 [Int], [CryptoCurrency]。

10、[String: T]:JSON 对象,用于字典结构,如 [“usd”: 1.0]。

11、enum + Codable:字符串/整数,用于表示固定范围的值

12、struct / class + Codable:JSON 对象,用于嵌套结构。

相关文章

1、自定义编码和解码逻辑(SwiftData中实现Codable协议): https://fangjunyu.com/2024/11/14/%e8%87%aa%e5%ae%9a%e4%b9%89%e7%bc%96%e7%a0%81%e5%92%8c%e8%a7%a3%e7%a0%81%e9%80%bb%e8%be%91%ef%bc%88swiftdata%e4%b8%ad%e5%ae%9e%e7%8e%b0codable%e5%8d%8f%e8%ae%ae%ef%bc%89/

2、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/

3、Swift Codable 深入解析:理解 CodingKeys 的关键角色:https://fangjunyu.com/2024/10/14/swift-codable-%e6%b7%b1%e5%85%a5%e8%a7%a3%e6%9e%90%ef%bc%9a%e7%90%86%e8%a7%a3-codingkeys-%e7%9a%84%e5%85%b3%e9%94%ae%e8%a7%92%e8%89%b2/

4、Swift为什么定义CodingKeys:https://fangjunyu.com/2024/11/12/swift%e4%b8%ba%e4%bb%80%e4%b9%88%e5%ae%9a%e4%b9%89codingkeys/

5、Swift静态属性与Codable的问题:https://fangjunyu.com/2024/12/24/swift%e9%9d%99%e6%80%81%e5%b1%9e%e6%80%a7%e4%b8%8ecodable%e7%9a%84%e9%97%ae%e9%a2%98/

6、Swift JSON数据解码保存到SwiftData:https://fangjunyu.com/2024/12/25/swift-json%e6%95%b0%e6%8d%ae%e8%a7%a3%e7%a0%81%e4%bf%9d%e5%ad%98%e5%88%b0swiftdata/

7、Swift UI解构JSON文件:https://fangjunyu.com/2024/10/03/swift-ui%e8%a7%a3%e6%9e%84json%e6%96%87%e4%bb%b6/

8、Swift JSON和对象的转换类JSONSerialization:https://fangjunyu.com/2024/11/01/swift-json%e5%92%8c%e5%af%b9%e8%b1%a1%e7%9a%84%e8%bd%ac%e6%8d%a2%e7%b1%bbjsonserialization/

9、Swift 网络请求方法URLSession.shared.dataTask:https://fangjunyu.com/2024/10/31/swift-%e7%bd%91%e7%bb%9c%e8%af%b7%e6%b1%82%e6%96%b9%e6%b3%95urlsession-shared-datatask/

10、Swift构建嵌套复杂的JSON的Codable模型结构:https://fangjunyu.com/2025/05/07/swift%e6%9e%84%e5%bb%ba%e5%b5%8c%e5%a5%97%e5%a4%8d%e6%9d%82%e7%9a%84json%e7%9a%84codable%e6%a8%a1%e5%9e%8b%e7%bb%93%e6%9e%84/

11、Swift JSONDecoder类:https://fangjunyu.com/2025/05/08/swift-jsondecoder%e7%b1%bb/

12、Swift使用JSONDecoder解码日期之ISO 8601 标准格式:https://fangjunyu.com/2024/11/11/swift%e4%bd%bf%e7%94%a8-iso-8601-%e6%a0%87%e5%87%86%e6%a0%bc%e5%bc%8f%e6%9d%a5%e8%a7%a3%e6%9e%90%e6%97%a5%e6%9c%9f/

13、Xcode Playground无法从Bundle读取文件问题:https://fangjunyu.com/2025/05/07/xcode-playground%e6%97%a0%e6%b3%95%e4%bb%8ebundle%e8%af%bb%e5%8f%96%e6%96%87%e4%bb%b6%e9%97%ae%e9%a2%98/

14、Swift处理和存储二进制数据的Data:https://fangjunyu.com/2024/11/21/swift%e5%a4%84%e7%90%86%e5%92%8c%e5%ad%98%e5%82%a8%e4%ba%8c%e8%bf%9b%e5%88%b6%e6%95%b0%e6%8d%ae%e7%9a%84data/

15、Swift UI 深入理解 DecodingError:https://fangjunyu.com/2024/10/05/swift-ui-%e6%b7%b1%e5%85%a5%e4%ba%86%e8%a7%a3decodingerror/

附录

相关代码

JSON文件:https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=100&page=1

SwiftUI代码:

import SwiftUI

struct ContentView: View {
    
    @State private var json:[CryptoDTO] = []

    func loadJSON() -> [CryptoDTO] {
        print("进入loadJSON方法")
        let url = URL(fileURLWithPath: #file)
            .deletingLastPathComponent()
            .appendingPathComponent("coins.json")
        print("url:\(url)")
        
        guard let data = try? Data(contentsOf: url) else {
            print("data读取失败")
            return []
        }
        
        let decoder = JSONDecoder()

        let formatter = DateFormatter()
            formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
            formatter.locale = Locale(identifier: "en_US_POSIX")
            decoder.dateDecodingStrategy = .formatted(formatter)
        do {
            print("进入do-catch方法")
            
            let cryptos = try decoder.decode([CryptoDTO].self, from: data)
            for crypto in cryptos {
                print("Name: \(crypto.name), Price: \(crypto.currentPrice)")
            }
            return cryptos
        } catch DecodingError.keyNotFound(let key, let context) {
            print("---1---")
            print("Key '\(key)' not found:", context.debugDescription)
            print("codingPath:", context.codingPath)
            return []
        } catch DecodingError.typeMismatch(let type, let context) {
            print("---2---")
            print("Type '\(type)' mismatch:", context.debugDescription)
            print("codingPath:", context.codingPath)
            return []
        } catch DecodingError.valueNotFound(let type, let context) {
            print("---3---")
            print("Value '\(type)' not found:", context.debugDescription)
            print("codingPath:", context.codingPath)
            return []
        } catch DecodingError.dataCorrupted(let context) {
            print("---4---")
            print("Data corrupted:", context.debugDescription)
            print("codingPath:", context.codingPath)
            return []
        } catch {
            print("Unexpected error: \(error)")
            return []
        }
    }
    var body: some View {
        Text("JSONDecoder")
            .onAppear {
                json = loadJSON()
            }
    }
}
   

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

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

发表回复

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