使用 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 而无需自定义编码的主要内容,如果未能解决你的问题,请参考以下文章
flatMap API 合约如何将可选输入转换为非可选结果?
如何在 Swift 4 中将 Encodable 或 Decodable 作为参数传递?