Swift Decodable:如何将ID注入嵌套泛型属性并设为非可选let
问题:将Strapi v4 API响应的ID转为非可选let属性注入子对象
Strapi v4 API响应结构
{ "data": [ { "id": 1, "attributes": { "description": "test" } } ] }
现有实现代码
用户编写的泛型处理代码如下,当前StrapiDataObjectContent协议中的id是可选的var,希望改为非可选的let属性:
struct StrapiArrayResponse<Content: StrapiDataObjectContent>: Codable { var data: [StrapiDataObject<Content>] } struct StrapiDataObject<Content: StrapiDataObjectContent>: Codable { let id: Int var attributes: Content init(from decoder: Decoder) throws { let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self) self.id = try container.decode(Int.self, forKey: .id) self.attributes = try container.decode(Content.self, forKey: .attributes) self.attributes.id = id } } protocol StrapiDataObjectContent: Codable { var id: Int! { get set } // 希望改为非可选的let属性 }
解决方案
方法一:让Content类型通过初始化器接收ID
这种方式逻辑直接,要求每个StrapiDataObjectContent实现类型必须提供带ID的初始化方法,解码时直接传递ID:
- 修改协议定义:
protocol StrapiDataObjectContent: Codable { let id: Int init(id: Int, from decoder: Decoder) throws }
- 调整
StrapiDataObject的解码逻辑:
struct StrapiDataObject<Content: StrapiDataObjectContent>: Codable { let id: Int let attributes: Content init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.id = try container.decode(Int.self, forKey: .id) // 获取attributes对应的解码器,传入ID初始化Content let attributesDecoder = try container.superDecoder(forKey: .attributes) self.attributes = try Content(id: id, from: attributesDecoder) } enum CodingKeys: String, CodingKey { case id, attributes } }
- 具体Content类型实现示例:
struct Post: StrapiDataObjectContent { let id: Int let description: String init(id: Int, from decoder: Decoder) throws { self.id = id let container = try decoder.container(keyedBy: CodingKeys.self) self.description = try container.decode(String.self, forKey: .description) } enum CodingKeys: String, CodingKey { case description } }
方法二:通过解码上下文传递ID
这种方式利用Swift解码的userInfo上下文传递ID,避免每个Content类型都重复写带ID的初始化逻辑:
- 定义自定义解码用户信息Key:
private struct StrapiIdKey: CodingUserInfoKey {}
- 调整
StrapiDataObject的解码逻辑,将ID注入上下文:
struct StrapiDataObject<Content: StrapiDataObjectContent>: Codable { let id: Int let attributes: Content init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.id = try container.decode(Int.self, forKey: .id) // 创建带有ID的解码器上下文 var attributesDecoder = try container.superDecoder(forKey: .attributes) attributesDecoder.userInfo[StrapiIdKey.self] = id self.attributes = try Content(from: attributesDecoder) } enum CodingKeys: String, CodingKey { case id, attributes } }
- 修改协议并实现Content类型:
protocol StrapiDataObjectContent: Codable { let id: Int } struct Post: StrapiDataObjectContent { let id: Int let description: String init(from decoder: Decoder) throws { // 从上下文获取ID guard let id = decoder.userInfo[StrapiIdKey.self] as? Int else { throw DecodingError.dataCorrupted( DecodingError.Context( codingPath: decoder.codingPath, debugDescription: "Strapi ID未在解码上下文中找到" ) ) } self.id = id let container = try decoder.container(keyedBy: CodingKeys.self) self.description = try container.decode(String.self, forKey: .description) } enum CodingKeys: String, CodingKey { case description } }
总结
- 方法一优势:逻辑直观,每个Content类型的初始化逻辑清晰,不依赖外部上下文;
- 方法二优势:Content类型代码更简洁,ID传递逻辑集中在
StrapiDataObject中,但需要依赖解码上下文,调试时需注意上下文注入是否正确。
内容的提问来源于stack exchange,提问作者heyfrank
相关产品推荐
相关产品推荐

