JSON 转 Swift Codable
粘贴一段 JSON 响应,得到能用 JSONDecoder 解码它的 Swift 类型:每个对象一个 Codable struct(或 final class),嵌套对象成为单独的类型并用单数命名,在某些示例里缺失或为 null 的属性写成可选,还附上配套的解码代码。示例无法确定的地方会明确标出,而不是乱猜。你的 JSON 只留在本页。
是合法 Swift 名称的键原样使用(user_name 仍是 user_name)。像 "my-key" 这样的键会得到驼峰名称和一条 CodingKeys。
字母、数字和下划线,其他字符会被清理掉。
关闭时,这些属性写成 String? 并带 // TODO 注释,由你决定类型。打开时,会生成一个小的 JSONValue 枚举,可以解码任何 JSON 值。
- 声明数
- 3
- 输入大小
- 345 B
示例无法确定这些类型
Root.manager在示例里全是 null
代码里每一处都标了 // TODO。String? 属性只能解码文本或 null,在你选定类型(或打开 JSONValue 选项)之前,遇到其他值解码会失败。
Swift 类型
import Foundation
struct Root: Codable, Identifiable {
let id: Int
let user_name: String
let is_active: Bool
let score: Double
let created_at: Date
let `default`: String
let manager: String? // TODO: only null in the sample; set the real type
let tags: [String]
let orders: [Order]
let address: Address
}
struct Order: Codable, Identifiable {
let id: String
let total: Double
let coupon: String?
}
struct Address: Codable {
let street: String
let zip: String
}
解码
放在你解码的地方。data 是收到的 Data,例如来自 URLSession。
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let value = try decoder.decode(Root.self, from: data)它是怎么工作的
- •用严格的解析器读取 JSON,并保留每个数字的原始写法,所以 1 是 Int,1.0 是 Double。某个字段一处是 1、另一处是 2.5,就写成 Double。
- •对象数组里的每个元素会合并成一个类型。某个键在部分元素里缺失,或在任何一个元素里为 null,属性就是可选的。
- •嵌套对象成为单独的类型,按键名的单数形式命名:items 变成 Item,categories 变成 Category。会遮住 Swift 或 Foundation 类型的名字(如 Data、Date)会在前面加上父类型名(RootData)。
- •不是合法 Swift 名称的键会得到驼峰命名的属性和一条 CodingKeys。default、class、self 等 Swift 关键字用反引号包起来。
- •ISO 8601 日期时间格式的字符串会成为 Date,解码代码会把 dateDecodingStrategy 设为 .iso8601;如果日期带小数秒(.iso8601 读不了),则给出一个简短的自定义解码策略。
- •对象有普通的 id 字段时加上 Identifiable。class 是 final 的,并且会写出 == 和 hash(into:),因为 Swift 只会为 struct 和 enum 提供它们。
它做不到的事
- •它只知道你的示例里有什么。真实响应里有时缺失、但在这里每个示例都出现的字段不会被写成可选;把几个示例放进一个数组里粘贴,就能覆盖到。
- •它不会把字符串值变成枚举。如果 status 总是 "open" 或 "closed",你也许想要一个 String 原始值的枚举,这要由你决定。
- •不是 ISO 8601 文本的日期,例如 Unix 时间戳或 "2026-03-07",会保留为 Int、Double 或 String。请用 .secondsSince1970 或 DateFormatter 处理。
- •不同键下结构相同的对象会生成不同的类型;如果它们其实是同一个东西,请手动合并。
- •它不生成 SwiftData 的 @Model 类,也不写自定义的 init(from:)。
- •属性名只用 ASCII 字符。完全由其他字符(例如中文)组成的键会得到 field 之类的名字,并用 CodingKeys 对应真实的键。
常见问题
- 该用 CodingKeys 还是 keyDecodingStrategy = .convertFromSnakeCase?
- 整个接口都统一用 snake_case 时用 .convertFromSnakeCase:类型更简洁,由解码器负责对应。键的风格不统一、含有连字符等字符,或者你希望无论用哪个解码器对应关系都成立时,用 CodingKeys。注意这个策略会把 user_ID 变成 userId,把 api_URL 变成 apiUrl,属性名必须与之完全一致。
- 我的 JSON 里明明有值,为什么属性是可选的?
- 因为在数组的至少一个元素里,它缺失或为 null。非可选的键缺失时 JSONDecoder 会抛出 keyNotFound,遇到 null 会抛出 valueNotFound,所以必须写成可选,每个元素才能解码成功。
- 为什么数字是 Double 而不是 Int?
- 因为它在某处带了小数点或指数(1.0、2.5、1e3),或者超出了 Int 的范围。JSONDecoder 不会把 2.5 解码成 Int,但可以把 2 解码成 Double,所以 Double 才能适用于见到的所有值。
- 我的日期带毫秒,.iso8601 解码失败,为什么?
- JSONDecoder 的 .iso8601 策略不接受 2026-03-07T17:50:56.123Z 这样的小数秒。示例里有小数秒时,解码代码会改用带 ISO8601DateFormatter 和 .withFractionalSeconds 的自定义策略,并在失败时再按整秒解析。
- 代码里的 TODO 注释是什么意思?
- 示例在那里看不出可用的类型:值总是 null、数组为空,或者字段有不同类型(一处是数字,另一处是文本)。属性写成 String? 让代码能编译,但它只能解码文本或 null。请选定真实类型,或打开 JSONValue 选项,得到一个能解码任何值的枚举。
- 用 struct 还是 class?
- 解码出的数据通常用 struct:它是值类型,属性满足条件时就是 Sendable,Swift 也会替你写好 Equatable 和 Hashable。需要引用语义时选 final class,例如在多个视图间共享同一个解码后的对象并就地修改。