【问题标题】:Use Swift's Encodable to encode optional properties as null without custom encoding使用 Swift 的 Encodable 将可选属性编码为 null 而无需自定义编码
【发布时间】: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}

【问题讨论】:

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

标签: ios json swift encoding codable


【解决方案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")])
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-03-13
        • 1970-01-01
        • 1970-01-01
        • 2021-01-11
        • 2018-12-21
        • 1970-01-01
        相关资源
        最近更新 更多