【问题标题】:Encode nil value as null with JSONEncoder使用 JSONEncoder 将 nil 值编码为 null
【发布时间】:2018-04-26 06:50:04
【问题描述】:

我正在使用 Swift 4 的 JSONEncoder。我有一个带有可选属性的Codable 结构,当值为nil 时,我希望此属性在生成的JSON 数据中显示为null 值。但是,JSONEncoder 会丢弃该属性并且不会将其添加到 JSON 输出中。有没有办法配置JSONEncoder,以便在这种情况下保留密钥并将其设置为null?

示例

下面的代码 sn-p 生成{"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)!)

【问题讨论】:

  • 写得很好的问题;)你清楚地说明了你想要什么以及你得到的当前结果。如果只有你的黑客同行会遵循这种风格......

标签: ios json swift4 codable


【解决方案1】:

是的,但您必须编写自己的 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 一致性编码了所有属性,为什么“手动编码所有属性”的工作方式不同呢?答案是一致性生成器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 中可能很难进行可配置......)

【讨论】:

  • 您是否愿意显示/链接哪个encode 重载将匹配可选的string 属性?在这里使用encodeNil(forKey:) 不是更好的方法(可读性)?
  • @PauloMattos 已编辑。
  • 感谢罗布的报道!我会(慢慢地)消化所有这些并带着更多问题回来;)现在,我猜当条件一致性(终于!)登陆Optional可编码实现会更安全......
  • 我创建了一个 Swift 错误报告,因为我需要这个功能。如果您也需要,请随时在此处添加您的想法。 bugs.swift.org/browse/SR-9232
【解决方案2】:

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

【讨论】:

  • 你应该替换为 ``` @propertyWrapper struct NullEncodable: Encodable where T: Encodable { var packedValue: T? func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() switch WrapValue { case .some(let value): try container.encode(value) case .none: try container.encodeNil() } } } ` `` 为了使用应用于JSONEncoder 的任何配置。
  • 我非常喜欢这个解决方案并对其进行了更新:将 ``` init(wrappedValue: T?) { self.wrappedValue = WrappedValue } ``` 添加到包装器类型中,以便隐式结构初始化器不要乱发脾气。
  • 发现了更多技巧!我将它们发布在一个要点上,因为它们太多,无法包含在此处的非格式化评论中...gist.github.com/mredig/f6d9efb196a25d857fe04a28357551a6 - 随时更新您的答案!
  • @mredig 显然伟大的思想都一样!这就是我在这里的完整实现中所拥有的:github.com/g-mark/NullCodable
  • @ChipsAndBits 好点。为此,您需要扩展KeyedDecodingContainer 以模拟decodeIfPresent(因为虽然包装的值是可选的,但属性包装器本身绝不是可选的)。我在github.com/g-mark/NullCodable 更新了repo。
【解决方案3】:

这是我们在项目中使用的一种方法。希望对您有所帮助。

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

【讨论】:

    【解决方案4】:

    我遇到了同样的问题。通过从结构创建字典而不使用 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 中的“名称”。

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

    小心嵌套结构:例如如果您有一个具有自定义结构作为类型的属性,您也需要将其转换为字典。您可以在 for 循环中执行此操作。

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

    【讨论】:

      【解决方案5】:

      正如@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 提供这么棒的库 ;)

      希望对你有帮助……

      【讨论】:

        【解决方案6】:

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

        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
        

        【讨论】:

          猜你喜欢
          • 2023-03-21
          • 1970-01-01
          • 2017-12-16
          • 2018-11-26
          • 2017-11-10
          • 1970-01-01
          • 2016-03-17
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多