深入理解 Swift Codable:从基础到进阶

目录

  1. [Codable 简介](#Codable 简介 "#codable-%E7%AE%80%E4%BB%8B")
  2. [与 HandyJSON 的差异](#与 HandyJSON 的差异 "#%E4%B8%8E-handyjson-%E7%9A%84%E5%B7%AE%E5%BC%82")
  3. 最小可用示例
  4. 字段映射:CodingKeys
  5. 可选值、缺失字段与默认值
    • 5.1 类型兼容的容错解码
    • 5.2 [@Default 属性包装器](#@Default 属性包装器 "#default-%E5%B1%9E%E6%80%A7%E5%8C%85%E8%A3%85%E5%99%A8")
  6. 枚举解析与回退策略
  7. [日期、Data 与 KeyStrategy](#日期、Data 与 KeyStrategy "#%E6%97%A5%E6%9C%9Fdata-%E4%B8%8E-keystrategy")
  8. 自定义编解码:进阶技巧
  9. 快速回顾
  10. 结语

本文示例代码仓库:github.com/wutao23yzd/...

Codable 简介

Codable 是 Swift 4 引入的协议组合,等价于 Encodable & Decodable。只要让自定义类型遵循 Codable,Swift 编译器即可 自动合成 JSON / Property-list 的编解码代码,免去了早期手写 init(from:) / encode(to:) 的大量样板。

场景:网络层 JSON ↔︎ Model、持久化、本地缓存、跨进程消息

与 HandyJSON 的差异

大多数项目中使用HandyJson作为序列化方案,因为使用方便,不需要考虑异常情况处理。但相较于 HandyJSON 依赖 Mirror 与 Objective-C Runtime 的反射机制、在编译期几乎不进行类型检查且遇到错配时常以 Any 兜底的"弱类型"方案,Codable 由 Swift 官方维护,采用编译期自动合成编解码逻辑,无需额外运行时注入,既避免了反射带来的性能开销,也让所有字段在编译阶段就能获得严格的类型安全保障;同时,Codable 纯值语义实现让大对象的 JSON 解析更高效、更易优化。综合 性能、类型安全 与 长期维护 三大维度,Codable 显著优于依赖第三方维护且潜在风险更多的 HandyJSON,因此在现代 iOS 项目中,使用 Codable 并逐步弃用 HandyJSON 是更可持续、可靠的选择。

Tips

  • HandyJSON 的反射式实现里,JSON 字段与模型属性类型不一致时,尝试用 Any 或默认值把解析继续做完。
  • 当我们说 "Codable 纯值语义实现更高效" 时,强调的是在大多数 JSON-Model 场景下,推荐用 struct(值类型)承载数据------能最大化编译期优化、减少 ARC 与指针跳转,让解析大 JSON 更快、更省内存。

最小可用示例

swift 复制代码
struct User: Codable {
    let id: Int
    var username: String
    var age: Int?
}

let json = "{\"id\":1,\"username\":\"Tom\",\"age\":21}".data(using: .utf8)!
let user = try? JSONDecoder().decode(User.self, from: json)

无需实现任何函数,User 即可在编译期获得自动合成的 init(from:)encode(to:)

字段映射:CodingKeys

后端接口常使用 snake_case(下划线分隔),而 Swift 倾向于 camelCase(驼峰)。可通过:

typescript 复制代码
struct User: Codable {
    var userId: Int
    var userName: String

    enum CodingKeys: String, CodingKey {
        case userId = "user_id"
        case userName = "user_name"
    }
}

如果全局皆为 snake_case,可以让 JSONDecoder 使用 keyDecodingStrategy = .convertFromSnakeCase,省去逐字段编写 CodingKeys

swift 复制代码
struct User: Codable {
  let userId: Int
  var userName: String
  var age: Int?
}
let json = "{\"user_id\":1,\"user_name\":\"Tom\", \"age\":21}".data(using: .utf8)!
      
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601
      
let user = try? decoder.decode(User.self, from: json)

可选值、缺失字段与默认值

类型兼容的容错解码

后端有时把数值字段当字符串返回,或反之。直接 decode(Int.self, forKey: ...) 遇到类型错配会抛错甚至导致 try? 解码为 nil。可通过 KeyedDecodingContainer 扩展兼容多种物理类型:

swift 复制代码
extension KeyedDecodingContainer {
    
    func decodeIfPresent(_ type: Int.Type, forKey key: Key) throws -> Int? {
        if let intValue = try? decode(Int.self, forKey: key) {
            return intValue
        }

        if let stringValue = try? decode(String.self, forKey: key),
           let intFromString = Int(stringValue) {
            return intFromString
        }
        return nil
    }
}

比如,下面的demo代码中,json字符串 age是字符串,User模型中ageInt类型

swift 复制代码
struct User: Codable {
  let id: Int
  var username: String
  var age: Int?
}
let json = "{\"id\":1,\"username\":\"Tom\",\"age\":\"21\"}".data(using: .utf8)!

let user = try? JSONDecoder().decode(User.self, from: json)

Tips

在绝大多数,Codable 的自动合成已经足够。属性类型的格式多变,比如:可以是Int、String、Int,以及动态不确定的情况,建议手写 init(from:)解码,(往往还要配对写 encode(to:))。

@Default 属性包装器

如果接口缺少某字段,Swift 默认会抛错;若字段标记为可选则变成 nil,但业务经常希望有 合理默认值。通过属性包装器封装一次即可:

swift 复制代码
protocol DefaultValue { 
    associatedtype Value: Codable
    static var defaultValue: Value { get } 
}

@propertyWrapper
struct Default<T: DefaultValue>: Codable {
    var wrappedValue: T.Value
}

extension Default: Codable {
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        wrappedValue = (try? container.decode(T.Value.self)) ?? T.defaultValue
    }
}

extension KeyedDecodingContainer {
  func decode<T>( _ type: Default<T>.Type, forKey key: Key) throws -> Default<T> where T: DefaultValue {
    try decodeIfPresent(type, forKey: key) ?? Default(wrappedValue: T.defaultValue)
   }
}

extension String {
    enum Empty: DefaultValue {
        static let defaultValue = ""
    }
    enum Zero: DefaultValue {
        static let defaultValue = "0"
    }
}

extension Int {
    enum Zero: DefaultValue {
        static let defaultValue = 0
    }
}

extension Bool {
    enum False: DefaultValue {
        static let defaultValue = false
    }
    enum True: DefaultValue {
        static let defaultValue = true
    }
}

extension Double {
    enum Zero: DefaultValue {
        static let defaultValue = 0.0
    }
}

extension Default {
    typealias True = Default<Bool.True>
    typealias False = Default<Bool.False>
    typealias EmptyString = Default<String.Empty>
    typealias ZeroString = Default<String.Zero>
    typealias ZeroDouble = Default<Double.Zero>
}

在下面的Demo代码中,json字符串中,只有2个字段,但User模型,却有6个字段,我们通过属性包装器添给相应字段加了默认值,如果不添加,Swift 默认会抛错。

swift 复制代码
struct User: Codable {
    let id: Int
    var username: String
    
    var age: Int?
    
    @Default.EmptyString
    var city: String
    
    @Default.True
    var gender: Bool
    
    @Default<Int.Zero>
    var count: Int
}

let json = "{\"id\":1,\"username\":\"Tom\"}".data(using: .utf8)!
let user = try? JSONDecoder().decode(User.self, from: json)

Tips

  • 建议为常用默认值(0""false 等)列出 TypeAlias,方便复用。
  • 避免盲目把所有字段设为可选并在业务层解包,集中在模型层兜底更安全。

枚举解析与回退策略

新增服务端枚举值,老版本 App 解析时会崩溃。解决思路:为枚举声明默认 case,在解码失败时兜底。

swift 复制代码
protocol CodableEnumeration: RawRepresentable, Codable where RawValue: Codable {
    static var defaultCase: Self { get }
}

extension CodableEnumeration {
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        do {
            let decoded = try container.decode(RawValue.self)
            self = Self.init(rawValue: decoded) ?? Self.defaultCase
        } catch {
            self = Self.defaultCase
        }
    }
}

下面的demo代码中,Jsongender为2,但Gender 枚举并没有定义对应的枚举值,于是解析成了unknown

swift 复制代码
enum Gender: Int, CodableEnumeration {
    case unknown = -1
    case male = 0
    case female = 1
    
    static let defaultCase: Self = .unknown
}

struct User: Codable {
    let id: Int
    var username: String
    
    var age: Int?
    
    var gender: Gender
    
    @Default.EmptyString
    var city: String
    
    @Default<Int.Zero>
    var count: Int
}
let json = "{\"id\":1,\"username\":\"Tom\", \"gender\": 2}".data(using: .utf8)!
let user = try? JSONDecoder().decode(User.self, from: json)

日期、Data 与 KeyStrategy

  • DateDecodingStrategy
    当用 JSONDecoder 把 JSON 里的时间字段解码成 Date 时,可以告诉解码器 "这个字段的格式是什么"
    • .iso8601:标准 RFC-3339
    • .secondsSince1970 / .millisecondsSince1970
    • .formatted(DateFormatter)------完全自定义
  • DataDecodingStrategy
    决定JSON 里的那段内容要如何还原成 Swift 的 Data,常用的策略只有两个
    • .base64 ------默认
    • .custom------自定义解析策略
  • keyDecodingStrategy / EncodingStrategy
    驼峰和下划线编解码时的一对互逆策略
    • .convertFromSnakeCase.convertToSnakeCase

示例: 日期解析

rust 复制代码
    let jsonISO   = #"{"created_at":"2025-06-07T12:34:56Z"}"#.data(using: .utf8)!
    let jsonSecs  = #"{"created_at":1720353296}"#.data(using: .utf8)!
    let jsonCustom = #"{"created_at":"07/06/2025 12:34"}"#.data(using: .utf8)!
    struct Payload: Codable { let createdAt: Date }
    
    let dec = JSONDecoder()
    dec.keyDecodingStrategy = .convertFromSnakeCase
    //  ISO-8601
    dec.dateDecodingStrategy = .iso8601
    print(try? dec.decode(Payload.self, from: jsonISO).createdAt)

    // 秒
    dec.dateDecodingStrategy = .secondsSince1970
    print(try? dec.decode(Payload.self, from: jsonSecs).createdAt)

    // 自定义格式
    let f = DateFormatter()
    f.dateFormat = "dd/MM/yyyy HH:mm"
    f.locale = Locale(identifier: "en_US_POSIX")
    f.timeZone = TimeZone(secondsFromGMT: 0)
    dec.dateDecodingStrategy = .formatted(f)
    print(try? dec.decode(Payload.self, from: jsonCustom).createdAt)

Data解析

swift 复制代码
// 1.base64
struct Avatar: Codable { let raw: Data }
let json = "{\"raw\":\"R0lGODlhAQABAIAAAACwAAAAAAQABAAA\"}".data(using: .utf8)!
let avatar = try? JSONDecoder().decode(Avatar.self, from: json)  // 默认.base64

// 2.自定义解析Data
struct Payload: Codable { let blob: Data }

let dec = JSONDecoder()
dec.dataDecodingStrategy = .custom { decoder in
    let container = try decoder.singleValueContainer()
    let hex = try container.decode(String.self)
    guard let data = Data(hexString: hex) else {
        throw DecodingError.dataCorruptedError(in: container,
            debugDescription: "Hex string is invalid")
    }
    return data
}

let json1 = "{\"blob\":\"48656c6c6f\"}".data(using: .utf8)!
let obj1  = try? dec.decode(Payload.self, from: json1)

自定义编解码:进阶技巧

泛型响应

在许多网络层封装中,常见的做法是使用泛型结构体作为响应模型,例如:

kotlin 复制代码
struct ApiResponse<T: Codable>: Codable {
    var code: Int
    var message: String
    var data: T?
}

当泛型参数 T 遵循 Codable 协议时,Swift 的编解码机制能够自动完成嵌套对象的递归解析,几乎无需手动干预 比如:

swift 复制代码
enum ErrorCode :Int, Codable {
    case Success = 0
    
    case Failed = -1
}

struct ApiCustomResponse<T: Codable>: Codable {
    var code: ErrorCode = .Failed
    var message: String?
    var data: T?
}

struct User: Codable {
  let id: Int
  var username: String
  var age: Int?
}

class NetWork {
   static func request<T: Codable>(_ modelType: T.Type = T.self) throws -> T {
       let json = "{\"code\":0, \"data\":{\"id\":1,\"username\":\"Tom\",\"age\":21}}".data(using: .utf8)!
       return try JSONDecoder().decode(modelType, from: json)
    }
}

do {
    let user = try NetWork.request(ApiCustomResponse<User>.self)
    print("\(user)")
} catch {
    print("\(error)")
}

示例中 JSON 字符串被成功解码为ApiCustomResponse<User>实例,data 字段为具体业务模型

多态模型

data 字段根据 kind 不同返回不同子结构,可在 init(from:) 中先 decode kind 再 switch 动态 decode ------ Swift 5.9 引入 any Codable 将进一步简化。

如下示例代码:同一个 data 节点会因为 kind 不同而呈现 完全不同的内部结构;在 init(from:) 里 先解标签再 switch,手动调用相应的 decode(SubType.self, ...)

swift 复制代码
enum Kind: String, Codable { case photo, video, audio }

protocol Media: Codable {}

struct Photo: Media  { let url: URL; let width: Int; let height: Int }
struct Video: Media  { let url: URL; let duration: Double; let codec: String }
struct Audio: Media  { let url: URL; let bitrate: Int }

struct Wrapper: Codable {
    let kind: Kind
    let data: Media          // ← 不同子类型都实现 Media

    enum CodingKeys: String, CodingKey { case kind, data }

    init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        kind = try c.decode(Kind.self, forKey: .kind)

        switch kind {
        case .photo:
            data = try c.decode(Photo.self, forKey: .data)
        case .video:
            data = try c.decode(Video.self, forKey: .data)
        case .audio:
            data = try c.decode(Audio.self, forKey: .data)
        }
    }

    func encode(to encoder: Encoder) throws {
        var c = encoder.container(keyedBy: CodingKeys.self)
        try c.encode(kind, forKey: .kind)

        switch data {
        case let p as Photo: try c.encode(p, forKey: .data)
        case let v as Video: try c.encode(v, forKey: .data)
        case let a as Audio: try c.encode(a, forKey: .data)
        default:
            throw EncodingError.invalidValue(data,
                .init(codingPath: c.codingPath, debugDescription: "Unknown media type"))
        }
    }
}

快速回顾

  • Codable 自动合成:遵循 Codable 即可获得编解码能力,避免手写样板。
  • 字段映射:CodingKeys + JSONDecoder.keyDecodingStrategy = .convertFromSnakeCase 两种思路兼容后端 snake_case。
  • 容错解码策略:@Default 属性包装器、带 defaultCase 的枚举、手写 init(from:) 兜底。
  • 进阶技巧:泛型响应、多态模型、日期 & Data 自定义策略。

结语

Codable 并非"能用即止"的黑盒。深入理解其自动合成规则、容错边界与自定义扩展点后,可以大幅提升 稳定性代码可维护性 。借助属性包装器与协议抽象,将解码 策略 前置到 Model 层,实现"上游正确,下游简单"。

相关推荐
tangweiguo030519874 小时前
SwiftUI布局完全指南:从入门到精通
ios·swift
T1an-18 小时前
最右IOS岗一面
ios
坏小虎11 小时前
Expo 快速创建 Android/iOS 应用开发指南
android·ios·rn·expo
光影少年12 小时前
Android和iOS原生开发的基础知识对RN开发的重要性,RN打包发布时原生端需要做哪些配置?
android·前端·react native·react.js·ios
北京自在科技12 小时前
Find My 修复定位 BUG,AirTag 安全再升级
ios·findmy·airtag
Digitally13 小时前
如何不用 USB 线将 iPhone 照片传到电脑?
ios·电脑·iphone
Sim14801 天前
iPhone将内置本地大模型,手机端AI实现0 token成本时代来临?
人工智能·ios·智能手机·iphone
Digitally1 天前
如何将 iPad 上的照片传输到 U 盘(4 种解决方案)
ios·ipad
报错小能手1 天前
ios开发方向——swift并发进阶核心 @MainActor 与 DispatchQueue.main 解析
开发语言·ios·swift
LcGero1 天前
Cocos Creator 业务与原生通信详解
android·ios·cocos creator·游戏开发·jsb