英文:
Omit generic optional with `JSONSerialiser`
问题
当使用 JSONSerialiser
对包含 Optional
属性的 Encodable
进行 encode
时,会省略 nil
值的编码,可能导致完全空白(紧凑)的结构。以下是关于这种行为的 XCTestCase
:
func test_whenNilValueIsSerialised_thenItIsOmitted() throws {
struct Container: Codable {
let cargo: Int?
}
let encoding = try XCTUnwrap(String(bytes: JSONEncoder().encode(Container(cargo: nil)), encoding: .utf8))
XCTAssertEqual(encoding, "{}")
}
然而,如果 Container
是泛型的,并且使用包装类型作为可选项进行实例化,则不会发生这种行为。以下测试用例失败:
func test_whenNilValueFromGenericIsSerialised_thenItIsOmitted() throws {
struct Container<Cargo: Codable>: Codable {
let cargo: Cargo
}
let container = Container<String?>(cargo: nil)
let encoding = try XCTUnwrap(String(bytes: JSONEncoder().encode(container), encoding: .utf8))
XCTAssertEqual(encoding, "{}")
}
失败的错误消息是:
XCTAssertEqual failed: ("{"cargo":null}") is not equal to ("{}")
问题: 是否有合理干净的解决方法,也许可以通过为 Container
实现显式的 encode(to:)
来解决?
注意:在这种情况下,以下解决方案不合适:
struct Container<Cargo: Codable>: Codable {
let cargo: Cargo?
}
...我希望在许多情况下能够使用非可选的 Cargo
。
英文:
When JSONSerialiser
is used to encode
an Encodable
containing an Optional
property, a nil
value is omitted from encoding, potentially leading to an entirely empty (and nice and compact) structure. Here's an XCTestCase
of this behaviour:
func test_whenNilValueIsSerialised_thenItIsOmitted() throws {
struct Container: Codable {
let cargo: Int?
}
let encoding = try XCTUnwrap(String(bytes: JSONEncoder().encode(Container(cargo: nil)), encoding: .utf8))
XCTAssertEqual(encoding, "{}")
}
However, if Container
is generic, and it is instantiated with the wrapped type as an optional, then this behaviour does not occur. The following test case fails:
func test_whenNilValueFromGenericIsSerialised_thenItIsOmitted() throws {
struct Container<Cargo: Codable>: Codable {
let cargo: Cargo
}
let container = Container<String?>(cargo: nil)
let encoding = try XCTUnwrap(String(bytes: JSONEncoder().encode(container), encoding: .utf8))
XCTAssertEqual(encoding, "{}")
}
The failure error is:
> XCTAssertEqual failed: ("{"cargo":null}") is not equal to ("{}")
The Question: Is there are reasonably clean work around for this, perhaps by implementing an explicit encode(to:)
for Container
?
NB: In this case, the following solution is not appropriate:
struct Container<Cargo: Codable>: Codable {
let cargo: Cargo?
}
… I want to be able to use a non optional as Cargo
in many cases.
答案1
得分: 1
我认为你需要像这个问题中所示那样做一些操作。你需要检查cargo
是否实际上是一个可选类型,并获取包装后的值。然后,对这个取消包装的值进行encodeIfPresent
。
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
let mirror = Mirror(reflecting: cargo)
if mirror.displayStyle == .optional {
let unwrapped = mirror.children.first?.value as? Encodable
try container.encodeIfPresent(unwrapped.map(AnyEncodable.init), forKey: .cargo)
} else {
try container.encode(cargo, forKey: .cargo)
}
}
struct AnyEncodable: Encodable {
let wrapped: any Encodable
func encode(to encoder: Encoder) throws {
try wrapped.encode(to: encoder)
}
}
请注意,由于取消包装的值的类型是Any?
,你需要将其转换为Encodable
,然后将其包装在具体类型(AnyEncodable
)中。
你也可以将这个操作写成KeyedEncodingContainer
的扩展:
extension KeyedEncodingContainer {
mutating func encodeGenericIfPresent<T: Encodable>(_ x: T, forKey key: Key) throws {
let mirror = Mirror(reflecting: x)
if mirror.displayStyle == .optional {
let unwrapped = mirror.children.first?.value as? Encodable
try encodeIfPresent(unwrapped.map(AnyEncodable.init), forKey: key)
} else {
try encode(x, forKey: key)
}
}
}
如果你编码一个嵌套的可选项,外部可选项不是nil
,但内部可选项是nil
,
JSONEncoder().encode(Container(cargo: String??.some(.none)))
它仍然会产生{"cargo": null}
。这与非泛型情况下合成的encode
实现一致,就像这样:
struct Container: Codable {
var cargo: String??
}
英文:
I think you would need to do something like in this question. You will need to check if cargo
actually is an optional type, and get the wrapped value. Then, encodeIfPresent
that unwrapped value.
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
let mirror = Mirror(reflecting: cargo)
if mirror.displayStyle == .optional {
let unwrapped = mirror.children.first?.value as? Encodable
try container.encodeIfPresent(unwrapped.map(AnyEncodable.init), forKey: .cargo)
} else {
try container.encode(cargo, forKey: .cargo)
}
}
struct AnyEncodable: Encodable {
let wrapped: any Encodable
func encode(to encoder: Encoder) throws {
try wrapped.encode(to: encoder)
}
}
Note that since the unwrapped value is of type Any?
, you would need to cast to Encodable
, and then wrap that in a concrete type (AnyEncodable
).
You can write this as an extension of KeyedEncodingContainer
too:
extension KeyedEncodingContainer {
mutating func encodeGenericIfPresent<T: Encodable>(_ x: T, forKey key: Key) throws {
let mirror = Mirror(reflecting: x)
if mirror.displayStyle == .optional {
let unwrapped = mirror.children.first?.value as? Encodable
try encodeIfPresent(unwrapped.map(AnyEncodable.init), forKey: key)
} else {
try encode(x, forKey: key)
}
}
}
If you encode a nested optional, where the outer optional is not nil, but the inner optional is nil,
JSONEncoder().encode(Container(cargo: String??.some(.none))
It would still produce {"cargo": null}
. This is consistent with the synthesised encode
implementations in non-generic cases like this:
struct Container: Codable {
var cargo: String??
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论