使用 JSONEncoder 将 nil 值编码为 null
Encode nil value as null with JSONEncoder
我正在使用 Swift 4 的 JSONEncoder
。我有一个带有可选 属性 的 Codable
结构,我希望这个 属性 在生成的 JSON 数据中显示为 null
值,当值为 nil
。但是,JSONEncoder
丢弃 属性 并且不将其添加到 JSON 输出。有没有办法配置 JSONEncoder
以便在这种情况下保留密钥并将其设置为 null
?
例子
下面的代码片段生成 {"number":1}
,但我更希望它给我 {"string":null,"number":1}
:
struct Foo: Codable {
var string: String? = nil
var number: Int = 1
}
let encoder = JSONEncoder()
let data = try! encoder.encode(Foo())
print(String(data: data, encoding: .utf8)!)
是的,但是您必须编写自己的 encode(to:)
实现,不能使用自动生成的。
struct Foo: Codable {
var string: String? = nil
var number: Int = 1
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(number, forKey: .number)
try container.encode(string, forKey: .string)
}
}
直接编码一个可选值将编码一个空值,就像您正在寻找的那样。
如果这对您来说是一个重要的用例,您可以考虑在 bugs.swift.org 打开一个缺陷,要求在 JSONEncoder 上添加一个新的 OptionalEncodingStrategy
标志以匹配现有的 DateEncodingStrategy
,等等(请参阅下文,为什么这可能不可能在今天的 Swift 中实际实施,但随着 Swift 的发展,进入跟踪系统仍然有用。)
编辑:对于下面 Paulo 的问题,这分派到通用 encode<T: Encodable>
版本,因为 Optional
符合 Encodable
。这是在 Codable.swift 中以这种方式实现的:
extension Optional : Encodable /* where Wrapped : Encodable */ {
@_inlineable // FIXME(sil-serialize-all)
public func encode(to encoder: Encoder) throws {
assertTypeIsEncodable(Wrapped.self, in: type(of: self))
var container = encoder.singleValueContainer()
switch self {
case .none: try container.encodeNil()
case .some(let wrapped): try (wrapped as! Encodable).__encode(to: &container)
}
}
}
这包装了对 encodeNil
的调用,我认为让 stdlib 将 Optionals 当作另一个 Encodable 来处理比在我们自己的编码器中将它们视为特殊情况并自己调用 encodeNil
更好。
另一个明显的问题是为什么它首先以这种方式工作。由于 Optional 是 Encodable,并且生成的 Encodable 一致性对所有属性进行编码,为什么 "encode all the properties by hand" 的工作方式不同?答案是一致性生成器 includes a special case for Optionals:
// Now need to generate `try container.encode(x, forKey: .x)` for all
// existing properties. Optional properties get `encodeIfPresent`.
...
if (varType->getAnyNominal() == C.getOptionalDecl() ||
varType->getAnyNominal() == C.getImplicitlyUnwrappedOptionalDecl()) {
methodName = C.Id_encodeIfPresent;
}
这意味着更改此行为将需要更改自动生成的一致性,而不是 JSONEncoder
(这也意味着在今天的 Swift 中可能真的很难配置......)
我运行陷入同样的问题。通过在不使用 JSONEncoder 的情况下从结构创建字典来解决它。你可以用一种相对通用的方式来做到这一点。这是我的代码:
struct MyStruct: Codable {
let id: String
let regionsID: Int?
let created: Int
let modified: Int
let removed: Int?
enum CodingKeys: String, CodingKey, CaseIterable {
case id = "id"
case regionsID = "regions_id"
case created = "created"
case modified = "modified"
case removed = "removed"
}
var jsonDictionary: [String : Any] {
let mirror = Mirror(reflecting: self)
var dic = [String: Any]()
var counter = 0
for (name, value) in mirror.children {
let key = CodingKeys.allCases[counter]
dic[key.stringValue] = value
counter += 1
}
return dic
}
}
extension Array where Element == MyStruct {
func jsonArray() -> [[String: Any]] {
var array = [[String:Any]]()
for element in self {
array.append(element.jsonDictionary)
}
return array
}
}
您可以在没有 CodingKeys 的情况下执行此操作(如果服务器端的 table 属性名称等于您的结构 属性 名称)。在这种情况下,只需使用 mirror.children 中的 'name'。
如果您需要 CodingKeys,请不要忘记添加 CaseIterable 协议。这使得使用 allCases 变量成为可能。
小心嵌套结构:例如如果您有一个 属性 类型为自定义结构,您也需要将其转换为字典。您可以在 for 循环中执行此操作。
如果要创建 MyStruct 字典数组,则需要 Array 扩展。
如@Peterdk 所述,已针对此问题创建错误报告:
https://bugs.swift.org/browse/SR-9232
如果你想坚持这个功能应该如何成为官方 API 在未来版本中的一部分,请随意投票。
而且,正如(Johan Nordberg)在这个错误报告中提到的,有一个库 FineJson 可以处理这个问题,而不必为所有可编码结构重写每个 encode(to:)
实现 ^ ^
这是一个示例,展示了我如何使用此库将 NULL
值编码到我的应用程序后端请求的 JSON 负载中:
import Foundation
import FineJSON
extension URLRequest {
init<T: APIRequest>(apiRequest: T, settings: APISettings) {
// early return in case of main conf failure
guard let finalUrl = URL(string: apiRequest.path, relativeTo: settings.baseURL) else {
fatalError("Bad resourceName: \(apiRequest.path)")
}
// call designated init
self.init(url: finalUrl)
var parametersData: Data? = nil
if let postParams = apiRequest.postParams {
do {
// old code using standard JSONSerializer :/
// parametersData = try JSONSerializer.encode(postParams)
// new code using FineJSON Encoder
let encoder = FineJSONEncoder.init()
// with custom 'optionalEncodingStrategy' ^^
encoder.optionalEncodingStrategy = .explicitNull
parametersData = try encoder.encode(postParams)
// set post params
self.httpBody = parametersData
} catch {
fatalError("Encoding Error: \(error)")
}
}
// set http method
self.httpMethod = apiRequest.httpMethod.rawValue
// set http headers if needed
if let httpHeaders = settings.httpHeaders {
for (key, value) in httpHeaders {
self.setValue(value, forHTTPHeaderField: key)
}
}
}
}
这些是我为处理此问题而必须执行的唯一更改。
感谢 Omochi 提供这个很棒的库 ;)
希望对您有所帮助...
这是我们在项目中使用的一种方法。希望对你有帮助。
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")])
我正在使用这个枚举来控制行为。我们的后端需要它:
public enum Tristate<Wrapped> : ExpressibleByNilLiteral, Encodable {
/// Null
case none
/// The presence of a value, stored as `Wrapped`.
case some(Wrapped)
/// Pending value, not none, not some
case pending
/// Creates an instance initialized with .pending.
public init() {
self = .pending
}
/// Creates an instance initialized with .none.
public init(nilLiteral: ()) {
self = .none
}
/// Creates an instance that stores the given value.
public init(_ some: Wrapped) {
self = .some(some)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .none:
try container.encodeNil()
case .some(let wrapped):
try (wrapped as! Encodable).encode(to: encoder)
case .pending: break // do nothing
}
}
}
typealias TriStateString = Tristate<String>
typealias TriStateInt = Tristate<Int>
typealias TriStateBool = Tristate<Bool>
/// 测试
struct TestStruct: Encodable {
var variablePending: TriStateString?
var variableSome: TriStateString?
var variableNil: TriStateString?
}
/// Structure with tristate strings:
let testStruc = TestStruct(/*variablePending: TriStateString(),*/ // pending, unresolved
variableSome: TriStateString("test"), // some, resolved
variableNil: TriStateString(nil)) // nil, resolved
/// Make the structure also tristate
let tsStruct = Tristate<TestStruct>(testStruc)
/// Make a json from the structure
do {
let jsonData = try JSONEncoder().encode(tsStruct)
print( String(data: jsonData, encoding: .utf8)! )
} catch(let e) {
print(e)
}
/// 输出
{"variableNil":null,"variableSome":"test"}
// variablePending is missing, which is a correct behaviour
这是一种使用 属性 包装器的方法(需要 Swift v5.1):
@propertyWrapper
struct NullEncodable<T>: Encodable where T: Encodable {
var wrappedValue: T?
init(wrappedValue: T?) {
self.wrappedValue = wrappedValue
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch wrappedValue {
case .some(let value): try container.encode(value)
case .none: try container.encodeNil()
}
}
}
示例用法:
struct Tuplet: Encodable {
let a: String
let b: Int
@NullEncodable var c: String? = nil
}
struct Test: Encodable {
@NullEncodable var name: String? = nil
@NullEncodable var description: String? = nil
@NullEncodable var tuplet: Tuplet? = nil
}
var test = Test()
test.tuplet = Tuplet(a: "whee", b: 42)
test.description = "A test"
let data = try JSONEncoder().encode(test)
print(String(data: data, encoding: .utf8) ?? "")
输出:
{
"name": null,
"description": "A test",
"tuplet": {
"a": "whee",
"b": 42,
"c": null
}
}
我正在使用 Swift 4 的 JSONEncoder
。我有一个带有可选 属性 的 Codable
结构,我希望这个 属性 在生成的 JSON 数据中显示为 null
值,当值为 nil
。但是,JSONEncoder
丢弃 属性 并且不将其添加到 JSON 输出。有没有办法配置 JSONEncoder
以便在这种情况下保留密钥并将其设置为 null
?
例子
下面的代码片段生成 {"number":1}
,但我更希望它给我 {"string":null,"number":1}
:
struct Foo: Codable {
var string: String? = nil
var number: Int = 1
}
let encoder = JSONEncoder()
let data = try! encoder.encode(Foo())
print(String(data: data, encoding: .utf8)!)
是的,但是您必须编写自己的 encode(to:)
实现,不能使用自动生成的。
struct Foo: Codable {
var string: String? = nil
var number: Int = 1
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(number, forKey: .number)
try container.encode(string, forKey: .string)
}
}
直接编码一个可选值将编码一个空值,就像您正在寻找的那样。
如果这对您来说是一个重要的用例,您可以考虑在 bugs.swift.org 打开一个缺陷,要求在 JSONEncoder 上添加一个新的 OptionalEncodingStrategy
标志以匹配现有的 DateEncodingStrategy
,等等(请参阅下文,为什么这可能不可能在今天的 Swift 中实际实施,但随着 Swift 的发展,进入跟踪系统仍然有用。)
编辑:对于下面 Paulo 的问题,这分派到通用 encode<T: Encodable>
版本,因为 Optional
符合 Encodable
。这是在 Codable.swift 中以这种方式实现的:
extension Optional : Encodable /* where Wrapped : Encodable */ {
@_inlineable // FIXME(sil-serialize-all)
public func encode(to encoder: Encoder) throws {
assertTypeIsEncodable(Wrapped.self, in: type(of: self))
var container = encoder.singleValueContainer()
switch self {
case .none: try container.encodeNil()
case .some(let wrapped): try (wrapped as! Encodable).__encode(to: &container)
}
}
}
这包装了对 encodeNil
的调用,我认为让 stdlib 将 Optionals 当作另一个 Encodable 来处理比在我们自己的编码器中将它们视为特殊情况并自己调用 encodeNil
更好。
另一个明显的问题是为什么它首先以这种方式工作。由于 Optional 是 Encodable,并且生成的 Encodable 一致性对所有属性进行编码,为什么 "encode all the properties by hand" 的工作方式不同?答案是一致性生成器 includes a special case for Optionals:
// Now need to generate `try container.encode(x, forKey: .x)` for all
// existing properties. Optional properties get `encodeIfPresent`.
...
if (varType->getAnyNominal() == C.getOptionalDecl() ||
varType->getAnyNominal() == C.getImplicitlyUnwrappedOptionalDecl()) {
methodName = C.Id_encodeIfPresent;
}
这意味着更改此行为将需要更改自动生成的一致性,而不是 JSONEncoder
(这也意味着在今天的 Swift 中可能真的很难配置......)
我运行陷入同样的问题。通过在不使用 JSONEncoder 的情况下从结构创建字典来解决它。你可以用一种相对通用的方式来做到这一点。这是我的代码:
struct MyStruct: Codable {
let id: String
let regionsID: Int?
let created: Int
let modified: Int
let removed: Int?
enum CodingKeys: String, CodingKey, CaseIterable {
case id = "id"
case regionsID = "regions_id"
case created = "created"
case modified = "modified"
case removed = "removed"
}
var jsonDictionary: [String : Any] {
let mirror = Mirror(reflecting: self)
var dic = [String: Any]()
var counter = 0
for (name, value) in mirror.children {
let key = CodingKeys.allCases[counter]
dic[key.stringValue] = value
counter += 1
}
return dic
}
}
extension Array where Element == MyStruct {
func jsonArray() -> [[String: Any]] {
var array = [[String:Any]]()
for element in self {
array.append(element.jsonDictionary)
}
return array
}
}
您可以在没有 CodingKeys 的情况下执行此操作(如果服务器端的 table 属性名称等于您的结构 属性 名称)。在这种情况下,只需使用 mirror.children 中的 'name'。
如果您需要 CodingKeys,请不要忘记添加 CaseIterable 协议。这使得使用 allCases 变量成为可能。
小心嵌套结构:例如如果您有一个 属性 类型为自定义结构,您也需要将其转换为字典。您可以在 for 循环中执行此操作。
如果要创建 MyStruct 字典数组,则需要 Array 扩展。
如@Peterdk 所述,已针对此问题创建错误报告:
https://bugs.swift.org/browse/SR-9232
如果你想坚持这个功能应该如何成为官方 API 在未来版本中的一部分,请随意投票。
而且,正如(Johan Nordberg)在这个错误报告中提到的,有一个库 FineJson 可以处理这个问题,而不必为所有可编码结构重写每个 encode(to:)
实现 ^ ^
这是一个示例,展示了我如何使用此库将 NULL
值编码到我的应用程序后端请求的 JSON 负载中:
import Foundation
import FineJSON
extension URLRequest {
init<T: APIRequest>(apiRequest: T, settings: APISettings) {
// early return in case of main conf failure
guard let finalUrl = URL(string: apiRequest.path, relativeTo: settings.baseURL) else {
fatalError("Bad resourceName: \(apiRequest.path)")
}
// call designated init
self.init(url: finalUrl)
var parametersData: Data? = nil
if let postParams = apiRequest.postParams {
do {
// old code using standard JSONSerializer :/
// parametersData = try JSONSerializer.encode(postParams)
// new code using FineJSON Encoder
let encoder = FineJSONEncoder.init()
// with custom 'optionalEncodingStrategy' ^^
encoder.optionalEncodingStrategy = .explicitNull
parametersData = try encoder.encode(postParams)
// set post params
self.httpBody = parametersData
} catch {
fatalError("Encoding Error: \(error)")
}
}
// set http method
self.httpMethod = apiRequest.httpMethod.rawValue
// set http headers if needed
if let httpHeaders = settings.httpHeaders {
for (key, value) in httpHeaders {
self.setValue(value, forHTTPHeaderField: key)
}
}
}
}
这些是我为处理此问题而必须执行的唯一更改。
感谢 Omochi 提供这个很棒的库 ;)
希望对您有所帮助...
这是我们在项目中使用的一种方法。希望对你有帮助。
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")])
我正在使用这个枚举来控制行为。我们的后端需要它:
public enum Tristate<Wrapped> : ExpressibleByNilLiteral, Encodable {
/// Null
case none
/// The presence of a value, stored as `Wrapped`.
case some(Wrapped)
/// Pending value, not none, not some
case pending
/// Creates an instance initialized with .pending.
public init() {
self = .pending
}
/// Creates an instance initialized with .none.
public init(nilLiteral: ()) {
self = .none
}
/// Creates an instance that stores the given value.
public init(_ some: Wrapped) {
self = .some(some)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .none:
try container.encodeNil()
case .some(let wrapped):
try (wrapped as! Encodable).encode(to: encoder)
case .pending: break // do nothing
}
}
}
typealias TriStateString = Tristate<String>
typealias TriStateInt = Tristate<Int>
typealias TriStateBool = Tristate<Bool>
/// 测试
struct TestStruct: Encodable {
var variablePending: TriStateString?
var variableSome: TriStateString?
var variableNil: TriStateString?
}
/// Structure with tristate strings:
let testStruc = TestStruct(/*variablePending: TriStateString(),*/ // pending, unresolved
variableSome: TriStateString("test"), // some, resolved
variableNil: TriStateString(nil)) // nil, resolved
/// Make the structure also tristate
let tsStruct = Tristate<TestStruct>(testStruc)
/// Make a json from the structure
do {
let jsonData = try JSONEncoder().encode(tsStruct)
print( String(data: jsonData, encoding: .utf8)! )
} catch(let e) {
print(e)
}
/// 输出
{"variableNil":null,"variableSome":"test"}
// variablePending is missing, which is a correct behaviour
这是一种使用 属性 包装器的方法(需要 Swift v5.1):
@propertyWrapper
struct NullEncodable<T>: Encodable where T: Encodable {
var wrappedValue: T?
init(wrappedValue: T?) {
self.wrappedValue = wrappedValue
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch wrappedValue {
case .some(let value): try container.encode(value)
case .none: try container.encodeNil()
}
}
}
示例用法:
struct Tuplet: Encodable {
let a: String
let b: Int
@NullEncodable var c: String? = nil
}
struct Test: Encodable {
@NullEncodable var name: String? = nil
@NullEncodable var description: String? = nil
@NullEncodable var tuplet: Tuplet? = nil
}
var test = Test()
test.tuplet = Tuplet(a: "whee", b: 42)
test.description = "A test"
let data = try JSONEncoder().encode(test)
print(String(data: data, encoding: .utf8) ?? "")
输出:
{
"name": null,
"description": "A test",
"tuplet": {
"a": "whee",
"b": 42,
"c": null
}
}