简体   繁体   English

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

[英]Use Swift's Encodable to encode optional properties as null without custom encoding

I want to encode an optional field with Swift's JSONEncoder using a struct that conforms to the Encodable protocol.我想使用符合Encodable协议的struct使用 Swift 的JSONEncoder对可选字段进行编码。

The default setting is that JSONEncoder uses the encodeIfPresent method, which means that values that are nil are excluded from the Json.默认设置是JSONEncoder使用encodeIfPresent方法,这意味着从 Json 中排除nil值。

How can I override this for a single property without writing my custom encode(to encoder: Encoder) function, in which I have to implement the encoding for all properties (like this article suggests under "Custom Encoding" )?如何在不编写我的自定义encode(to encoder: Encoder)函数的情况下为单个属性覆盖它,我必须在其中实现所有属性的编码(如本文在“自定义编码”下建议)?

Example:例子:

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}
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()

This will give you output after executing main:这将在执行 main 后为您提供输出:

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

So, you can see that we encoded gender as it'll be null in dictionary.因此,您可以看到我们对性别进行了编码,因为它在字典中将为空。 The only limitation you'll get is that you'll have to access optional value via value property您将获得的唯一限制是您必须通过value属性访问可选值

If you try to decode this JSON your trusty JSONDecoder will create exactly the same object as exemplified in this Playground:如果您尝试解码此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)
}

This will print这将打印

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

so from a "standard" Swift point of view your distinction makes very little sense.因此,从“标准”Swift 的角度来看,您的区分意义不大。 You can of course determine it, but the path is thorny and leads through the purgatory of JSONSerialization or [String:Any] decoding and a lot more ugly optionals.您当然可以确定它,但路径是棘手的,并且需要通过JSONSerialization[String:Any]解码的炼狱以及更多丑陋的选项。 Of course if you are serving another language with your interface that might make sense, but still I consider it a rather rare case which easily merits the implementation of encode(to encoder: Encoder) which is not hard at all, just a little tedious to clarify your slightly non-standard behaviour.当然,如果您使用可能有意义的界面提供另一种语言,但我仍然认为这是一种相当罕见的情况,很容易实现encode(to encoder: Encoder)这一点都不难,只是有点乏味澄清你稍微不标准的行为。

This looks like a fair compromise to me.这对我来说似乎是一个公平的妥协。

You can use something like this to encode single values.您可以使用这样的东西来编码单个值。

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)
        }
    }
}

And the usage is like this用法是这样的

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")])

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM