使用JSONEncoder将nil值编码为null

dr_*_*rto 39 json ios swift4 codable

我正在使用Swift 4 JSONEncoder.我有一个Codable带有可选属性的结构,我希望这个属性在null值为时在生成的JSON数据中显示为值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)!)
Run Code Online (Sandbox Code Playgroud)

Rob*_*ier 34

是的,但你必须编写自己的编码器; 你不能使用默认的.

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)
    }
}
Run Code Online (Sandbox Code Playgroud)

直接编码可选项将编码null,就像您正在寻找的那样.

如果这是一个重要的用例,你可以考虑在bugs.swift.org上打开一个缺陷,要求在JSONEncoderencode(to:)添加一个新的标志以匹配现有的OptionalEncodingStrategy等等.(见下文为什么这可能实际上是不可能的)今天在Swift中实现,但是随着Swift的发展,进入跟踪系统仍然很有用.)


编辑:对于下面的Paulo的问题,这将调度到通用DateEncodingStrategy版本,因为encode<T: Encodable>符合Optional.这是通过这种方式在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)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这包含了调用Encodable,我认为让stdlib处理Optionals只是另一个Encodable比在我们自己的编码器中将它们视为一个特殊情况并调用encodeNil自己更好.

另一个显而易见的问题是为什么它首先以这种方式工作.由于Optional是Encodable,并且生成的Encodable一致性对所有属性进行编码,为什么"手动编码所有属性"的工作方式不同?答案是一致性生成器包含一个特殊情况: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;
}
Run Code Online (Sandbox Code Playgroud)

这意味着改变这种行为需要改变自动生成的一致性,而不是encodeNil(这也意味着它可能很难在今天的Swift中进行配置......)

  • 您是否愿意显示/链接哪个 `encode` 重载将匹配可选的 `string` 属性?在这里使用 `encodeNil(forKey:)` 不是更好的方法(可读性明智)吗? (2认同)

g-m*_*ark 24

这是一种使用属性包装器的方法(需要 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()
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

示例用法:

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) ?? "")
Run Code Online (Sandbox Code Playgroud)

输出:

{
  "name": null,
  "description": "A test",
  "tuplet": {
    "a": "whee",
    "b": 42,
    "c": null
  }
}
Run Code Online (Sandbox Code Playgroud)

完整实现在这里:https : //github.com/g-mark/NullCodable

  • @mredig 显然英雄所见略同!这就是我在这里完整实现的内容:https://github.com/g-mark/NullCodable (3认同)
  • @ChipsAndBits 好点。为了实现这一点,您需要扩展 `KeyedDecodingContainer` 来模拟 `decodeIfPresent` (因为虽然包装的值是可选的,但属性包装器本身从来不是可选的)。我在 https://github.com/g-mark/NullCodable 更新了存储库。 (3认同)