使用 Swift 的 Encodable 将可选属性编码为 null 而无需自定义编码

Posted

技术标签:

【中文标题】使用 Swift 的 Encodable 将可选属性编码为 null 而无需自定义编码【英文标题】:Use Swift's Encodable to encode optional properties as null without custom encoding 【发布时间】:2018-04-27 16:26:16 【问题描述】:

我想使用符合Encodable 协议的struct 使用Swift 的JSONEncoder 对可选字段进行编码。

默认设置是JSONEncoder 使用encodeIfPresent 方法,这意味着nil 的值被排除在Json 之外。

如何在不编写我的自定义 encode(to encoder: Encoder) 函数的情况下为单个属性覆盖它,我必须在其中实现所有属性的编码(如 this article 在“自定义编码”下建议)?

例子:

struct MyStruct: Encodable 
    let id: Int
    let date: Date?


let myStruct = MyStruct(id: 10, date: nil)
let jsonData = try JSONEncoder().encode(myStruct)
print(String(data: jsonData, encoding: .utf8)!) // "id":10

【问题讨论】:

相关问题,但改用自定义编码逻辑:***.com/questions/47266862/… 您到底想达到什么目的?哈希中的JSON 条目,例如"date": null;?您打算通过明确表达null 来传达什么不同?如果您打算使用 Swift 来使用结果,那么一开始您将很难分辨出其中的差异。您的链接似乎是对encodeIfPresent 的唯一值得注意的参考,但这种情况似乎非常罕见,值得实施encode(to encoder: Encoder) 我的 API 通过显式设置 null 来重置值。而且根据我的经验,这种情况并不罕见...... 如果不实现您自己的encode,我认为这是不可能的。 (您需要覆盖的 JSONEncoder 部分是fileprivate。)如果实现起来并不简单,我建议您使用 SwiftGen 来编写它;这应该很容易在 SwiftGen 中构建。通常,不可能获得半定制的 Encodables。有少量非常具体的配置点,但除此之外,它目前是默认的或自定义的。我希望这会有所改善。 【参考方案1】:
import Foundation

enum EncodableOptional<Wrapped>: ExpressibleByNilLiteral 
    case none
    case some(Wrapped)
    init(nilLiteral: ()) 
        self = .none
    


extension EncodableOptional: Encodable where Wrapped: Encodable 

    func encode(to encoder: Encoder) throws 
        var container = encoder.singleValueContainer()
        switch self 
        case .none:
            try container.encodeNil()
        case .some(let wrapped):
            try wrapped.encode(to: encoder)
        
    


extension EncodableOptional

    var value: Optional<Wrapped> 

        get 
            switch self 
            case .none:
                return .none
            case .some(let v):
                return .some(v)
            
        

        set 
            switch newValue 
            case .none:
                self = .none
            case .some(let v):
                self = .some(v)
            
        
    


struct User: Encodable 
    var name: String
    var surname: String
    var age: Int?
    var gender: EncodableOptional<String>


func main() 
    var user = User(name: "William", surname: "Lowson", age: 36, gender: nil)
    user.gender.value = "male"
    user.gender.value = nil
    print(user.gender.value ?? "")
    let jsonEncoder = JSONEncoder()
    let data = try! jsonEncoder.encode(user)
    let json = try! JSONSerialization.jsonObject(with: data, options: [])
    print(json)

    let dict: [String: Any?] = [
        "gender": nil
    ]
    let d = try! JSONSerialization.data(withJSONObject: dict, options: [.prettyPrinted])
    let j = try! JSONSerialization.jsonObject(with: d, options: [])
    print(j)


main()

这将在执行 main 后给你输出:


    age = 36;
    gender = "<null>";
    name = William;
    surname = Lowson;


    gender = "<null>";

因此,您可以看到我们对性别进行了编码,因为它在字典中为空。您将获得的唯一限制是您必须通过 value 属性访问可选值

【讨论】:

【参考方案2】:

如果您尝试解码此 JSON,您可信赖的 JSONDecoder 将创建与此 Playground 中示例完全相同的对象:

import Cocoa

struct MyStruct: Codable 
    let id: Int
    let date: Date?


let jsonDataWithNull = """
    
        "id": 8,
        "date":null
    
    """.data(using: .utf8)!

let jsonDataWithoutDate = """
    
        "id": 8
    
    """.data(using: .utf8)!

do 
    let withNull = try JSONDecoder().decode(MyStruct.self, from: jsonDataWithNull)
    print(withNull)
 catch 
    print(error)


do 
    let withoutDate = try JSONDecoder().decode(MyStruct.self, from: jsonDataWithoutDate)
    print(withoutDate)
 catch 
    print(error)

这将打印出来

MyStruct(id: 8, date: nil)
MyStruct(id: 8, date: nil)

因此,从“标准”Swift 的角度来看,您的区别几乎没有意义。您当然可以确定它,但路径是棘手的,并通过JSONSerialization[String:Any] 解码的炼狱和更多丑陋的选项。当然,如果您使用可能有意义的界面提供另一种语言,但我仍然认为这是一个相当罕见的情况,很容易实现 encode(to encoder: Encoder) 的实现,这并不难,只是有点乏味来澄清你的轻微非- 标准行为。

这对我来说似乎是一个公平的妥协。

【讨论】:

【参考方案3】:

您可以使用类似这样的方式对单个值进行编码。

struct CustomBody: Codable 
    let method: String
    let params: [Param]

    enum CodingKeys: String, CodingKey 
        case method = "method"
        case params = "params"
    


enum Param: Codable 
    case bool(Bool)
    case integer(Int)
    case string(String)
    case stringArray([String])
    case valueNil
    case unsignedInteger(UInt)
    case optionalString(String?)

    init(from decoder: Decoder) throws 
        let container = try decoder.singleValueContainer()
        if let x = try? container.decode(Bool.self) 
            self = .bool(x)
            return
        
        if let x = try? container.decode(Int.self) 
            self = .integer(x)
            return
        
        if let x = try? container.decode([String].self) 
              self = .stringArray(x)
              return
          
        if let x = try? container.decode(String.self) 
            self = .string(x)
            return
        
        if let x = try? container.decode(UInt.self) 
            self = .unsignedInteger(x)
            return
        
        throw DecodingError.typeMismatch(Param.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for Param"))
    

    func encode(to encoder: Encoder) throws 
        var container = encoder.singleValueContainer()
        switch self 
        case .bool(let x):
            try container.encode(x)
        case .integer(let x):
            try container.encode(x)
        case .string(let x):
            try container.encode(x)
        case .stringArray(let x):
            try container.encode(x)
        case .valueNil:
            try container.encodeNil()
        case .unsignedInteger(let x):
            try container.encode(x)
        case .optionalString(let x):
            x?.isEmpty == true ? try container.encodeNil() : try container.encode(x)
        
    

而且用法是这样的

RequestBody.CustomBody(method: "WSDocMgmt.getDocumentsInContentCategoryBySearchSource", 
                       params: [.string(legacyToken), .string(shelfId), .bool(true), .valueNil, .stringArray(queryFrom(filters: filters ?? [])), .optionalString(sortMethodParameters()), .bool(sortMethodAscending()), .unsignedInteger(segment ?? 0), .unsignedInteger(segmentSize ?? 0), .string("NO_PATRON_STATUS")])

【讨论】:

以上是关于使用 Swift 的 Encodable 将可选属性编码为 null 而无需自定义编码的主要内容,如果未能解决你的问题,请参考以下文章

将可选数据写入 Firebase

flatMap API 合约如何将可选输入转换为非可选结果?

如何在 Swift 4 中将 Encodable 或 Decodable 作为参数传递?

Swift 中可选标识符中感叹号的含义? [复制]

协议类型“Encodable”的值不能符合“Encodable”;只有结构/枚举/类类型可以符合协议

定义后无法将可选参数传递给express