【问题标题】:JSONEncoder won't allow type encoded to primitive valueJSONEncoder 不允许类型编码为原始值
【发布时间】:2023-03-21 22:10:02
【问题描述】:

我正在为具有可能关联值的enum 类型实现Codable。由于这些对于每种情况都是独一无二的,我认为我可以在编码期间不带密钥地输出它们,然后简单地看看我在解码时可以得到什么以恢复正确的情况。

这是一个非常精简的人为示例,演示了一种动态类型的值:

enum MyValueError : Error { case invalidEncoding }

enum MyValue {
    case bool(Bool)
    case float(Float)
    case integer(Int)
    case string(String)
}

extension MyValue : Codable {
    init(from theDecoder:Decoder) throws {
        let theEncodedValue = try theDecoder.singleValueContainer()

        if let theValue = try? theEncodedValue.decode(Bool.self) {
            self = .bool(theValue)
        } else if let theValue = try? theEncodedValue.decode(Float.self) {
            self = .float(theValue)
        } else if let theValue = try? theEncodedValue.decode(Int.self) {
            self = .integer(theValue)
        } else if let theValue = try? theEncodedValue.decode(String.self) {
            self = .string(theValue)
        } else { throw MyValueError.invalidEncoding }
    }

    func encode(to theEncoder:Encoder) throws {
        var theEncodedValue = theEncoder.singleValueContainer()
        switch self {
        case .bool(let theValue):
            try theEncodedValue.encode(theValue)
        case .float(let theValue):
            try theEncodedValue.encode(theValue)
        case .integer(let theValue):
            try theEncodedValue.encode(theValue)
        case .string(let theValue):
            try theEncodedValue.encode(theValue)
        }
    }
}

let theEncodedValue = try! JSONEncoder().encode(MyValue.integer(123456))
let theEncodedString = String(data: theEncodedValue, encoding: .utf8)
let theDecodedValue = try! JSONDecoder().decode(MyValue.self, from: theEncodedValue)

但是,这在编码阶段给了我一个错误,如下所示:

 "Top-level MyValue encoded as number JSON fragment."

问题似乎是,无论出于何种原因,JSONEncoder 都不允许将不是可识别原语的顶级类型编码为单个原语值。如果我将singleValueContainer() 更改为unkeyedContainer(),那么它工作得很好,除了生成的JSON 是一个数组,而不是单个值,或者我可以使用一个键控容器,但这会产生一个带有增加了密钥的开销。

我在这里尝试用单值容器做的事情是不可能的吗?如果没有,我可以使用一些解决方法吗?

我的目标是让我的类型Codable 的开销最小,而不仅仅是JSON(该解决方案应该支持任何有效的Encoder/Decoder)。

【问题讨论】:

  • 您的类型中的可编码实现没有问题(尽管您应该在解码时交换浮点/整数大小写,否则任何整数都会陷入浮点大小写),JSONEncoder/Decoder 只是没有t 支持对不是数组/字典的顶级对象进行编码。如果当您实际使用此类型时,您将其用作另一个可编码对象的属性,那么它将正常工作。
  • 一个简单的解决方案是将值包装在一个数组中,例如let theEncodedValue = try! JSONEncoder().encode([MyValue.integer(123456)]) 然后用let theDecodedValue = try! JSONDecoder().decode([MyValue].self, from: theEncodedValue)解码
  • 实际上,进行该更改表明@dan 是正确的,它确实将数字解析为浮点数,而不是整数。

标签: swift codable


【解决方案1】:

有一个错误报告:

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

SR-6163:JSONDecoder 无法解码 RFC 7159 JSON

基本上,从 RFC-7159 开始,像 123 这样的值是有效的 JSON,但 JSONDecoder 不支持它。您可以跟进错误报告以查看未来对此的任何修复。 [从 iOS 13 开始,该错误已修复。]

#失败的地方#

失败在下面这行代码,你可以看到如果对象不是数组也不是字典,就会失败:

https://github.com/apple/swift-corelibs-foundation/blob/master/Foundation/JSONSerialization.swift#L120

open class JSONSerialization : NSObject {
        //...

        // top level object must be an Swift.Array or Swift.Dictionary
        guard obj is [Any?] || obj is [String: Any?] else {
            return false
        }

        //...
} 

#解决方法#

您可以使用JSONSerialization,带有选项:.allowFragments:

let jsonText = "123"
let data = Data(jsonText.utf8)

do {
    let myString = try JSONSerialization.jsonObject(with: data, options: .allowFragments)
    print(myString)
}
catch {
    print(error)
}

编码成键值对

最后,您还可以让您的 JSON 对象如下所示:

{ "integer": 123456 }

或

{ "string": "potatoe" }

为此,您需要执行以下操作:

import Foundation 

enum MyValue {
    case integer(Int)
    case string(String)
}

extension MyValue: Codable {
    
    enum CodingError: Error { 
        case decoding(String) 
    }
    
    enum CodableKeys: String, CodingKey { 
        case integer
        case string 
    }

    init(from decoder: Decoder) throws {

        let values = try decoder.container(keyedBy: CodableKeys.self)

        if let integer = try? values.decode(Int.self, forKey: .integer) {
            self = .integer(integer)
            return
        }

        if let string = try? values.decode(String.self, forKey: .string) {
            self = .string(string)
            return
        }

        throw CodingError.decoding("Decoding Failed")
    }


    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodableKeys.self)

        switch self {
            case let .integer(i):
            try container.encode(i, forKey: .integer)
            case let .string(s):
            try container.encode(s, forKey: .string)
        }
    }

}

let theEncodedValue = try! JSONEncoder().encode(MyValue.integer(123456))
let theEncodedString = String(data: theEncodedValue, encoding: .utf8)
print(theEncodedString!) // { "integer": 123456 }
let theDecodedValue = try! JSONDecoder().decode(MyValue.self, from: theEncodedValue)

【讨论】:

  • Ack,嗯,那是一个经典的掌脸时刻;当然,这种类型并不打算作为顶级元素,所以问题出在我的测试上,而不是实现上。感谢您的出色回答!
  • 哦,作为注释;我确实尝试了一个键/值选项,并找到了一种巧妙的方法。通过拉出values.allKeys.first,您实际上可以使用一个开关立即执行正确的解码,而不是一次尝试所有这些,特别是在很多情况下都很方便;我只是不喜欢向编码格式添加密钥的开销。
  • 太棒了。感谢旁注!
猜你喜欢
  • 1970-01-01
  • 2016-04-04
  • 1970-01-01
  • 2012-06-11
  • 2011-07-21
  • 1970-01-01
  • 2018-04-26
  • 2016-03-02
  • 1970-01-01
相关资源
最近更新 更多