使用`JSONSerialiser`省略通用可选项。

huangapple go评论75阅读模式
英文:

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, &quot;{}&quot;)
}

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&lt;Cargo: Codable&gt;: Codable {
        let cargo: Cargo
    }

    let container = Container&lt;String?&gt;(cargo: nil)
    let encoding = try XCTUnwrap(String(bytes: JSONEncoder().encode(container), encoding: .utf8))

    XCTAssertEqual(encoding, &quot;{}&quot;)
}

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&lt;Cargo: Codable&gt;: 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&lt;T: Encodable&gt;(_ 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 {&quot;cargo&quot;: null}. This is consistent with the synthesised encode implementations in non-generic cases like this:

struct Container: Codable {
    var cargo: String??
}

huangapple
  • 本文由 发表于 2023年7月3日 15:15:07
  • 转载请务必保留本文链接:https://go.coder-hub.com/76602589.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定