Swift Codable 进阶:从基础到复杂类型

8 10.6~13.6 分钟

我们都知道,Codable = Encodable + Decodable。

Swift 给了我们一套几乎“傻瓜式”的序列化 / 反序列化方案。

但是,除了基础类型(String, Int, Double, Bool 等),一旦遇到 枚举、自定义 struct、Date、URL,事情就没那么轻松了。

下面我就用几个小故事 + 示例,聊聊我踩过的坑和应对方法。

1. 基础类型 —— “最省心”的部分

struct User: Codable {
  var id: Int
  var name: String
}

JSON 只要对得上 key,就能自动 decode:

{ "id": 1, "name": "Alice" }

这里没啥坑,但后续就开始复杂了。

2. 枚举 (enum) —— “多选一”的麻烦

我在写一个任务管理 App 时,遇到接口里有个 status 字段:

{ "status": "done" }

最自然的写法是:

enum Status: String, Codable {
  case todo
  case doing
  case done
}

这样就能自动解码了。

但坑在于:后端有时候会传奇怪值

比如 "status": "archived",我的枚举里没有,就会解码失败。

解决办法:

✅ 写 init(from:)容错

enum Status: String, Codable {
  case todo, doing, done, unknown

  init(from decoder: Decoder) throws {
    let container = try decoder.singleValueContainer()
    let raw = try container.decode(String.self)
    self = Status(rawValue: raw) ?? .unknown
  }
}

这样就算来了 "archived",也能落到 .unknown。

3. 自定义 struct —— “嵌套的考验”

比如一个活动对象,里面有主办方信息:

{
  "title": "Swift Meetup",
  "organizer": {
    "name": "Capton",
    "url": "https://example.com"
  }
}


我们定义两个 struct:

struct Organizer: Codable {
  var name: String
  var url: URL
}

struct Event: Codable {
  var title: String
  var status: Status
  var date: Date
  var organizer: Organizer
}

这时候 Codable 会自动帮我们把 organizer 映射成嵌套 struct,非常优雅。

但是,一旦 url 变成 "not_a_url",就又踩坑了。

URL解码意外的情况

当测试环境的接口里,url 字段变成了:

"url": "not_a_valid_url"

这时候问题就来了:

  • JSONDecoder 会尝试用 URL(string:) 来初始化。

  • 但 not_a_valid_url 显然不是合法地址,于是 decode 直接抛错。

如果我用 do-catch,就会在 catch 里收到 DecodingError.dataCorrupted,拿不到这个资源对象:

do {
  let resource = try JSONDecoder().decode(Resource.self, from: data)
  print(resource)
} catch {
  print("解码失败:\(error)")
}

虽然 App 不会崩,但数据缺失的问题很麻烦,尤其是业务里 url 可能是非必填字段。

所以我干脆自己写了个“安全 URL 包装器”:

struct SafeURL: Codable {
  var value: URL?

  init(from decoder: Decoder) throws {
    let container = try decoder.singleValueContainer()
    let urlString = try container.decode(String.self)
    value = URL(string: urlString)
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.singleValueContainer()
    try container.encode(value?.absoluteString ?? "")
  }
}

这样一来,就算后端传来奇怪的字符串,SafeURL.value 也只是 nil,

不会影响整个模型的解码成功。

此时Organizer变为了

struct Organizer: Codable {
  var name: String
  var url: SafeURL
}

4. Date —— “时间格式的战争”

小故事:我和后端的“时间格式的战争”


有一次我在写一个活动 App,要从接口拿到活动时间。

于是我很天真地写了个 model:

struct Event: Codable {
  var title: String
  var status: Status 
  //  其他字段
}

接口返回的是这样一段 JSON:

{
  "title": "Swift 社区见面会",
  "date": "2025-09-27 14:30:00"
}

我信心满满地跑起来,结果直接抛错。

因为 Swift 默认把 Date 编解码成 时间戳(Double),而后端给的是个字符串。

好嘛,我的 App 和后端同事就开始了“时间大战”:

  • 我说:“你们给时间戳吧,我这边直接就能解了。”

  • 后端说:“不行,我们 Java 统一用字符串,别搞特殊。”

于是我只能乖乖写解码策略:

let decoder = JSONDecoder()
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
decoder.dateDecodingStrategy = .formatted(formatter)

这下才算是把时间读进来。

后来我干脆封装了一个 DateFormatter 工具,项目里全局用,不然每个地方都写一次太麻烦了。

5. 混合案例 —— “大而全”的对象

结合上面所有情况:

{
  "title": "Swift Workshop",
  "status": "done",
  "date": "2025-09-27 14:30:00",
  "organizer": {
    "name": "Capton",
    "url": "not_a_valid_url"
  }
}

模型:

struct Event: Codable {
  var title: String
  var status: Status
  var date: Date
  var organizer: Organizer
}

配合:

  • 自定义 Status 容错

  • DateDecodingStrategy 格式化

  • SafeURL 包装

就能完美解码,即使后端数据再怎么“放飞自我”。

代码demo

CodableDemo.playground.zip (重命名去除 .zip)

6. 我的经验总结

  • enum:加个 .unknown 容错,防止后端乱传值。

  • struct 嵌套:没坑,但子 struct 要考虑容错。

  • Date:永远确认前后端的时间格式,最好写统一的解码器。

  • URL:写 SafeURL,让模型健壮性更高。

  • 复杂 JSON:别怕写 init(from:),关键场景手写反而最安全。

📌 一句话总结:

Codable 能帮我们 80%,剩下的 20% 靠“自定义 + 容错”。

下一篇 Swift 里的 struct、class、actor 我是怎么选的