使用JSONEncoder将nil值编码为null

95

我正在使用 Swift 4 的 JSONEncoder。我有一个带有可选属性的 Codable 结构体,并且当值为 nil 时,我希望该属性在生成的 JSON 数据中显示为 null 值。然而,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)!)

24
非常好的问题 ;) 你清楚地陈述了你想要什么和你目前得到的结果。如果你的同行黑客也能遵循这种风格就好了... - Paulo Mattos
6个回答

78

可以,但你需要编写自己的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)
    }
}

将可选项直接编码将会编码一个null,就像你所期望的那样。

如果这对你来说是一个重要的用例,你可以考虑在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的调用,我认为让标准库把Optionals作为另一种可编码类型来处理,比我们自己在编码器中将它们视为特殊情况并调用encodeNil更好。

另一个显而易见的问题是,为什么它首先可以按照这种方式工作。由于Optional是可编码的,生成的可编码符合对所有属性进行编码的要求,为什么手动“编码所有属性”会有所不同?答案是符合生成器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 中很难进行配置...)。

2
你能否展示/链接哪个encode重载会匹配可选的string属性?而且在这里使用encodeNil(forKey:)不是更好的方法(从可读性角度来看)吗? - Paulo Mattos
@PauloMattos 已编辑。 - Rob Napier
1
感谢Rob的撰写!我会(慢慢地)消化这些内容,并回来提出更多问题;)目前,我猜测当条件一致性(终于!)落实时,Optional可编码实现将会更加安全... - Paulo Mattos
1
我创建了一个Swift错误报告,因为我需要这个功能。如果您也需要此功能,请随时在那里添加您的想法。https://bugs.swift.org/browse/SR-9232 - Peterdk

52
这里提供一种使用属性包装器的方法(需要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
  }
}

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


1
为了使用应用于 JSONEncoder 的任何配置,您应该替换为以下内容:@propertyWrapper struct NullEncodable: Encodable where T: Encodable { var wrappedValue: T? 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() } } } - Dario Scoppelletti
1
发现了更多的技巧!我把它们发布在一个 gist 上,因为它们太多了,无法包含在这里非格式化的评论中... https://gist.github.com/mredig/f6d9efb196a25d857fe04a28357551a6 - 随意从中更新你的答案! - mredig
3
@mredig 显然英雄所见略同!这也是我在完整实现中的做法:https://github.com/g-mark/NullCodable - Steven Grosmark
1
@ Steven Grosmark 当缺少密钥时,解码似乎无法正常工作。它会抛出DecodingError.keyNotFound异常。如果我删除@NullEncodable包装器,那么它就可以正常工作。有什么想法吗? - ChipsAndBits
3
不错的观点。为了实现这一点,您需要扩展“KeyedDecodingContainer”来模拟“decodeIfPresent”(因为尽管包装值是可选的,但属性包装器本身永远不会是可选的)。我已经更新了 https://github.com/g-mark/NullCodable 上的存储库。 - Steven Grosmark
显示剩余4条评论

1

这是我们在一个项目中使用的方法。希望它能够有所帮助。

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")])

1

我正在使用这个枚举来控制行为。这是我们后端要求的。

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

你能添加它的可解码函数吗?(我的意思是,你能让 Tristate 符合 Codable 协议吗?) - jpulikkottil

0

我遇到了相同的问题。通过从结构中创建一个字典而没有使用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的情况下完成此操作。在这种情况下,只需使用mirror.children中的'name'即可。

如果需要使用CodingKeys,请不要忘记添加CaseIterable协议。这使得可以使用allCases变量。

注意嵌套结构体:例如,如果您有一个自定义结构体类型的属性,则还需要将其转换为字典。您可以在for循环中完成此操作。

如果要创建MyStruct字典数组,则需要使用Array扩展。


0

正如@Peterdk所提到的,已经针对此问题创建了一个错误报告:

https://bugs.swift.org/browse/SR-9232

如果您希望坚持认为此功能应该成为将来版本官方API的一部分,可以随意给它投票。

正如在此错误报告中(由Johan Nordberg提到)提到的那样,有一个名为FineJson的库可以处理此问题,而无需为所有可编码结构体重写每个encode(to:)实现 ^^

这里是一个示例,展示如何使用此库使能够在我的应用程序后端请求的JSON有效载荷中编码NULL值:

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提供的这个优秀库;)

希望能有所帮助...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接