如何从Swift Codable中排除属性?

172

Swift 4发布的Encodable/Decodable协议使得JSON序列化和反序列化变得非常愉悦。然而,我尚未找到一种方法来精细地控制哪些属性应该进行编码,哪些属性应该进行解码。

我注意到,在相应的CodingKeys枚举中排除属性会完全排除该属性,但是否有更精细的控制方式呢?


你是在说你有一种情况,其中你有一些属性想要编码,但是你想要解码不同的属性吗?(即,你希望你的类型不能往返转换?)因为如果你只关心排除该属性,给它一个默认值并将其从CodingKeys枚举中省略即可。 - Itai Ferber
1
无论如何,您始终可以手动实现 Codable 协议的要求(init(from:)encode(to:)),以完全控制该过程。 - Itai Ferber
我的特定用例是避免给解码器过多的控制权,这可能导致远程获取的JSON覆盖内部属性值。以下解决方案是足够的! - RamwiseMatt
3
我希望看到一个答案/新的Swift功能,它只需要处理特殊情况和被排除的键,而不是重新实现通常可以免费获得的所有属性。 - pkamb
7个回答

296

需要进行编码/解码的键列表由类型 CodingKeys(注意结尾处的s)控制。编译器可以为您合成它,但始终可以覆盖它。

假设您想从编码解码中排除属性nickname

struct Person: Codable {
    var firstName: String
    var lastName: String
    var nickname: String?
    
    private enum CodingKeys: String, CodingKey {
        case firstName, lastName
    }
}

如果您希望它是不对称的(即编码但不解码或反之亦然),则必须提供自己的实现 encode(with encoder: )init(from decoder: )

struct Person: Codable {
    var firstName: String
    var lastName: String
    
    // Since fullName is a computed property, it's excluded by default
    var fullName: String {
        return firstName + " " + lastName
    }

    private enum CodingKeys: String, CodingKey {
        case firstName, lastName, fullName
    }

    // We don't want to decode `fullName` from the JSON
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        firstName = try container.decode(String.self, forKey: .firstName)
        lastName = try container.decode(String.self, forKey: .lastName)
    }

    // But we want to store `fullName` in the JSON anyhow
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(firstName, forKey: .firstName)
        try container.encode(lastName, forKey: .lastName)
        try container.encode(fullName, forKey: .fullName)
    }
}

27
为使其正常工作,您需要为nickname 设置一个默认值。否则,在 init(from:) 中无法为该属性分配任何值。 - Itai Ferber
3
我已切换为可选项,这是我最初在Xcode中使用的。 - Code Different
1
@MarqueIV 是的,你必须这样做。由于fullName无法映射到存储属性,所以你必须提供自定义的编码器和解码器。 - Code Different
2
刚在 Swift 5 中测试了这个。你只需要为你不想解码的属性定义一个常量即可。你不需要显式地将键添加到 CodingKeys 中。因此,var nickname: String { get { "name" } } 应该就足够了。 - Leo
1
@Leo 抱歉,什么?在不定义CodingKeys来排除昵称的情况下,有没有办法按照答案中的顶部示例进行操作,假设它是一个普通的可读写属性? - Dan Rosenstark
显示剩余3条评论

44

使用自定义属性包装器的解决方案

struct Person: Codable {
    var firstName: String
    var lastName: String
    
    @CodableIgnored
    var nickname: String?
}

CodableIgnored 是什么

@propertyWrapper
public struct CodableIgnored<T>: Codable {
    public var wrappedValue: T?
        
    public init(wrappedValue: T?) {
        self.wrappedValue = wrappedValue
    }
    
    public init(from decoder: Decoder) throws {
        self.wrappedValue = nil
    }
    
    public func encode(to encoder: Encoder) throws {
        // Do nothing
    }
}

extension KeyedDecodingContainer {
    public func decode<T>(
        _ type: CodableIgnored<T>.Type,
        forKey key: Self.Key) throws -> CodableIgnored<T>
    {
        return CodableIgnored(wrappedValue: nil)
    }
}

extension KeyedEncodingContainer {
    public mutating func encode<T>(
        _ value: CodableIgnored<T>,
        forKey key: KeyedEncodingContainer<K>.Key) throws
    {
        // Do nothing
    }
}

1
这似乎完美地适用于您希望几乎所有内容都使用Codability的默认实现,除了一两个成员之外的情况。如果要承担整个自定义Codable符合性的复杂性,那将是一件遗憾的事情,而此方法通过封装其他地方的复杂性并使使用站点适当地最小化来防止这种情况发生。属性包装器胜利! - Porter Child
1
这两个 KeyedDecodingContainerKeyedEncodingContainer 的扩展真的有必要吗?在我的情况下,没有它们编码和解码也能正常工作。 - Nickkk
是的,它们是必需的。如果没有这段代码,被标记为@CodableIgnored的字段将无法被解码器解析。 - Ivan

7

另一种排除某些属性不被编码器编码的方法是使用单独的编码容器。

struct Person: Codable {
    let firstName: String
    let lastName: String
    let excludedFromEncoder: String

    private enum CodingKeys: String, CodingKey {
        case firstName
        case lastName
    }
    private enum AdditionalCodingKeys: String, CodingKey {
        case excludedFromEncoder
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let anotherContainer = try decoder.container(keyedBy: AdditionalCodingKeys.self)
        firstName = try container.decode(String.self, forKey: .firstName)
        lastName = try container.decode(String.self, forKey: .lastName)

        excludedFromEncoder = try anotherContainer(String.self, forKey: . excludedFromEncoder)
    }

    // it is not necessary to implement custom encoding
    // func encode(to encoder: Encoder) throws

    // let person = Person(firstName: "fname", lastName: "lname", excludedFromEncoder: "only for decoding")
    // let jsonData = try JSONEncoder().encode(person)
    // let jsonString = String(data: jsonData, encoding: .utf8)
    // jsonString --> {"firstName": "fname", "lastName": "lname"}

}

同样的方法也可以用于解码器


解码器在执行解码时是否会自动选择正确的容器,还是我需要为此定义一个条件逻辑? - Coder
@Citizen_5解码器具有使用两种编码键枚举(常规CodingKeys和自定义枚举)的逻辑,但编码器只知道CodingKeys,这就是为什么它以这种方式工作的原因。 - Rukshan

5
如果我们需要从一个大型属性集合的结构中排除几个属性的解码,将它们声明为可选属性。解包可选项的代码比在 CodingKey 枚举下编写很多键更少。
我建议使用扩展添加计算实例属性和计算类型属性。这样可以将符合 Codable 的属性与其他逻辑分开,提高可读性。

4
你可以使用计算属性:
struct Person: Codable {
  var firstName: String
  var lastName: String
  var nickname: String?

  var nick: String {
    get {
      nickname ?? ""
    }
  }

  private enum CodingKeys: String, CodingKey {
    case firstName, lastName
  }
}

5
这是我的线索 - 使用lazy var有效地将其变成运行时属性,从而将其排除在可编码范围之外。 - ChrisH
计算属性不能懒加载,你能详细阐述一下你的想法吗?@ChrisH - Bruno Muniz
1
@BrunoMuniz,老实说我已经记不清为什么会发表这个评论了,但我很可能是在提到类似于lazy var x:String = { //Computed string }()这样的东西,它可以代替计算属性,尽管我真的想象不出来为什么要这样做。 - ChrisH
ChrisH的解决方案很棒!这不是计算属性,而是使用延迟初始化闭包。 - undefined

0

虽然这是可以做到的,但最终结果会变得非常不符合 Swift 的风格,甚至不符合 JSON 的规范。我明白你的想法,因为在 HTML 中,#id 是很常见的概念,但它很少被用于 JSON 中,这是我认为的一件好事。

如果您使用递归哈希重构您的 JSON 文件,即如果您的 recipe 只包含一个 ingredients 数组,该数组又包含(一个或多个)ingredient_info,那么某些 Codable 结构体将能够很好地解析您的 JSON 文件。这样,解析器将帮助您首先将网络拼接在一起,而您只需要通过简单遍历结构来提供一些反向链接,如果您真的需要它们的话。由于这需要对您的 JSON 和数据结构进行彻底的重新设计,因此我只是简单地为您勾勒了这个想法。如果您认为这是可行的,请在评论中告诉我,然后我可以进一步详细说明,但根据情况,您可能无法自由更改其中任何一个。


0

我使用协议及其扩展与AssociatedObject一起设置和获取图像(或任何需要从Codable中排除的属性)属性。

这样我们就不必实现自己的编码器和解码器了。

以下是代码,为简单起见仅保留相关代码:

protocol SCAttachmentModelProtocol{
    var image:UIImage? {get set}
    var anotherProperty:Int {get set}
}
extension SCAttachmentModelProtocol where Self: SCAttachmentUploadRequestModel{
    var image:UIImage? {
        set{
            //Use associated object property to set it
        }
        get{
            //Use associated object property to get it
        }
    }
}
class SCAttachmentUploadRequestModel : SCAttachmentModelProtocol, Codable{
    var anotherProperty:Int
}

现在,每当我们想要访问实现(SCAttachmentModelProtocol)协议的对象上的Image属性时,我们可以使用它。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接