也许社区中有人遇到过类似的困扰,并提出了可行的解决方案。
我们目前正在开发多语言键/值存储。 鉴于此,我们通常不知道提前存储什么。
考虑以下结构
struct Character : Codable, Equatable {
let name: String
let age: Int
let gender: Gender
let hobbies: [String]
static func ==(lhs: Character, rhs: Character) -> Bool {
return (lhs.name == rhs.name
&& lhs.age == rhs.age
&& lhs.gender == rhs.gender
&& lhs.hobbies == rhs.hobbies)
}
}
通过线路发送/接收角色实体时,一切都相当简单。 用户可以向我们提供我们可以解码的类型。
但是,我们确实有能力动态查询存储在后端的实体。 例如,我们可以请求“name”属性的值并返回该值。
这种活力是一个痛点。 除了不知道属性的类型(除了它们是可编码的之外)之外,返回的格式也可以是动态的。
以下是提取属性的两个不同调用的响应示例:
{"value":"Bilbo"}
和
{"value":["[Ljava.lang.Object;",["Bilbo",111]]}
在某些情况下,它可能相当于一本字典。
现在,我有以下结构来处理响应:
fileprivate struct ScalarValue<T: Decodable> : Decodable {
var value: T?
}
使用字符示例,传递给解码器的类型将是:
ScalarValue<Character>.self
但是,对于单值、数组或字典的情况,我有点卡住了。
我从这样的事情开始:
fileprivate struct AnyDecodable: Decodable {
init(from decoder: Decoder) throws {
// ???
}
}
根据我上面描述的可能的返回类型,我不确定当前的 API 是否可以实现。
想法?
Swift 绝对可以处理任意 JSON 解码。这与任意可解码不同。 JSON 无法对所有可能的值进行编码。但这种结构将解码任何可以用 JSON 表达的内容,从那里您可以以类型安全的方式探索它,而无需求助于像
Any
这样危险且尴尬的工具。
enum JSON: Decodable, CustomStringConvertible {
var description: String {
switch self {
case .string(let string): return "\"\(string)\""
case .number(let double):
if let int = Int(exactly: double) {
return "\(int)"
} else {
return "\(double)"
}
case .object(let object):
return "\(object)"
case .array(let array):
return "\(array)"
case .bool(let bool):
return "\(bool)"
case .null:
return "null"
}
}
var isEmpty: Bool {
switch self {
case .string(let string): return string.isEmpty
case .object(let object): return object.isEmpty
case .array(let array): return array.isEmpty
case .null: return true
case .number, .bool: return false
}
}
struct Key: CodingKey, Hashable, CustomStringConvertible {
var description: String {
return stringValue
}
var hashValue: Int { return stringValue.hash }
static func ==(lhs: JSON.Key, rhs: JSON.Key) -> Bool {
return lhs.stringValue == rhs.stringValue
}
let stringValue: String
init(_ string: String) { self.stringValue = string }
init?(stringValue: String) { self.init(stringValue) }
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
}
case string(String)
case number(Double) // FIXME: Split Int and Double
case object([Key: JSON])
case array([JSON])
case bool(Bool)
case null
init(from decoder: Decoder) throws {
if let string = try? decoder.singleValueContainer().decode(String.self) { self = .string(string) }
else if let number = try? decoder.singleValueContainer().decode(Double.self) { self = .number(number) }
else if let object = try? decoder.container(keyedBy: Key.self) {
var result: [Key: JSON] = [:]
for key in object.allKeys {
result[key] = (try? object.decode(JSON.self, forKey: key)) ?? .null
}
self = .object(result)
}
else if var array = try? decoder.unkeyedContainer() {
var result: [JSON] = []
for _ in 0..<(array.count ?? 0) {
result.append(try array.decode(JSON.self))
}
self = .array(result)
}
else if let bool = try? decoder.singleValueContainer().decode(Bool.self) { self = .bool(bool) }
else {
self = .null
}
}
var objectValue: [String: JSON]? {
switch self {
case .object(let object):
let mapped: [String: JSON] = Dictionary(uniqueKeysWithValues:
object.map { (key, value) in (key.stringValue, value) })
return mapped
default: return nil
}
}
var arrayValue: [JSON]? {
switch self {
case .array(let array): return array
default: return nil
}
}
subscript(key: String) -> JSON? {
guard let jsonKey = Key(stringValue: key),
case .object(let object) = self,
let value = object[jsonKey]
else { return nil }
return value
}
var stringValue: String? {
switch self {
case .string(let string): return string
default: return nil
}
}
var doubleValue: Double? {
switch self {
case .number(let number): return number
default: return nil
}
}
var intValue: Int? {
switch self {
case .number(let number): return Int(number)
default: return nil
}
}
subscript(index: Int) -> JSON? {
switch self {
case .array(let array): return array[index]
default: return nil
}
}
var boolValue: Bool? {
switch self {
case .bool(let bool): return bool
default: return nil
}
}
}
有了这个,你可以做这样的事情:
let bilboJSON = """
{"value":"Bilbo"}
""".data(using: .utf8)!
let bilbo = try! JSONDecoder().decode(JSON.self, from: bilboJSON)
bilbo["value"] // "Bilbo"
let javaJSON = """
{"value":["[Ljava.lang.Object;",["Bilbo",111]]}
""".data(using: .utf8)!
let java = try! JSONDecoder().decode(JSON.self, from: javaJSON)
java["value"]?[1] // ["Bilbo", 111]
java["value"]?[1]?[0]?.stringValue // "Bilbo" (as a String rather than a JSON.string)
?
的扩散有点难看,但是在我的实验中使用throws
并没有真正让界面变得更好(特别是因为下标不能抛出)。根据您的特定用例,可能建议进行一些调整。
我自己为此目的编写了一个 AnyCodable 结构体:
struct AnyCodable: Decodable {
var value: Any
struct CodingKeys: CodingKey {
var stringValue: String
var intValue: Int?
init?(intValue: Int) {
self.stringValue = "\(intValue)"
self.intValue = intValue
}
init?(stringValue: String) { self.stringValue = stringValue }
}
init(value: Any) {
self.value = value
}
init(from decoder: Decoder) throws {
if let container = try? decoder.container(keyedBy: CodingKeys.self) {
var result = [String: Any]()
try container.allKeys.forEach { (key) throws in
result[key.stringValue] = try container.decode(AnyCodable.self, forKey: key).value
}
value = result
} else if var container = try? decoder.unkeyedContainer() {
var result = [Any]()
while !container.isAtEnd {
result.append(try container.decode(AnyCodable.self).value)
}
value = result
} else if let container = try? decoder.singleValueContainer() {
if let intVal = try? container.decode(Int.self) {
value = intVal
} else if let doubleVal = try? container.decode(Double.self) {
value = doubleVal
} else if let boolVal = try? container.decode(Bool.self) {
value = boolVal
} else if let stringVal = try? container.decode(String.self) {
value = stringVal
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "the container contains nothing serialisable")
}
} else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Could not serialise"))
}
}
}
extension AnyCodable: Encodable {
func encode(to encoder: Encoder) throws {
if let array = value as? [Any] {
var container = encoder.unkeyedContainer()
for value in array {
let decodable = AnyCodable(value: value)
try container.encode(decodable)
}
} else if let dictionary = value as? [String: Any] {
var container = encoder.container(keyedBy: CodingKeys.self)
for (key, value) in dictionary {
let codingKey = CodingKeys(stringValue: key)!
let decodable = AnyCodable(value: value)
try container.encode(decodable, forKey: codingKey)
}
} else {
var container = encoder.singleValueContainer()
if let intVal = value as? Int {
try container.encode(intVal)
} else if let doubleVal = value as? Double {
try container.encode(doubleVal)
} else if let boolVal = value as? Bool {
try container.encode(boolVal)
} else if let stringVal = value as? String {
try container.encode(stringVal)
} else {
throw EncodingError.invalidValue(value, EncodingError.Context.init(codingPath: [], debugDescription: "The value is not encodable"))
}
}
}
}
它也适用于嵌套字典/数组。您可以在 Playground 中使用任何 json 进行尝试。
let decoded = try! JSONDecoder().decode(AnyCodable.self, from: jsonData)
是的,可以通过现有的
Codable
API 实现您所描述的内容,并且以一种优雅的方式我会说(尽管我在这里可能很主观,因为我正在谈论我的代码:))。
让我们尝试找出此任务需要什么:
首先,您需要将所有属性声明为可选。这是必需的,因为解码器可能必须处理部分响应。
struct Character: Codable {
let name: String?
let age: Int?
let hobbies: [String]?
}
接下来,我们需要一种方法来弄清楚如何将结构属性映射到部分 JSON 中的各个字段。幸运的是,
Codable
API 可以通过 CodingKeys
枚举来帮助我们:
enum CodingKeys: String, CodingKey {
case name
case age
case hobbies
}
第一个棘手的部分是以某种方式将
CodingKeys
枚举转换为字符串数组,我们可以将其用于数组响应 - {"value":["[Ljava.lang.Object;",["Bilbo",111]]}
。我们很幸运,互联网上有各种资源可以解决获取枚举的所有情况的问题。我首选的解决方案是 RawRepresentable
扩展,因为 CodingKey
是原始可表示的,它的原始值是 String
:
// Adds support for retrieving all enum cases. Since we refer a protocol here,
// theoretically this method can be called on other types than enum
public extension RawRepresentable {
static var enumCases: [Self] {
var caseIndex: Int = 0
return Array(AnyIterator {
defer { caseIndex += 1 }
return withUnsafePointer(to: &caseIndex) {
$0.withMemoryRebound(to: Self.self, capacity: 1) { $0.pointee }
}
})
}
}
我们已经快完成了,但在解码之前我们还需要做一些工作。
现在我们有了一个
Decodable
类型,即要使用的编码键列表,我们需要一个使用它们的解码器。但在此之前,我们需要能够识别可以部分解码的类型。让我们添加一个新协议
protocol PartiallyDecodable: Decodable {
associatedtype PartialKeys: RawRepresentable
}
并使
Character
符合它
struct Character : Codable, PartiallyDecodable {
typealias PartialKeys = CodingKeys
最后的部分是解码部分。我们可以复用标准库自带的
JSONDecoder
:
// Tells the form of data the server sent and we want to decode:
enum PartialDecodingStrategy {
case singleKey(String)
case arrayOfValues
case dictionary
}
extension JSONDecoder {
// Decodes an object by using a decoding strategy
func partialDecode<T>(_ type: T.Type, withStrategy strategy: PartialDecodingStrategy, from data: Data) throws -> T where T : PartiallyDecodable, T.PartialKeys.RawValue == String {
将上述所有结果连接到以下基础设施中:
// Adds support for retrieving all enum cases. Since we refer a protocol here,
// theoretically this method can be called on other types than enum
public extension RawRepresentable {
static var enumCases: [Self] {
var caseIndex: Int = 0
return Array(AnyIterator {
defer { caseIndex += 1 }
return withUnsafePointer(to: &caseIndex) {
$0.withMemoryRebound(to: Self.self, capacity: 1) { $0.pointee }
}
})
}
}
protocol PartiallyDecodable: Decodable {
associatedtype PartialKeys: RawRepresentable
}
// Tells the form of data the server sent and we want to decode:
enum PartialDecodingStrategy {
case singleKey(String)
case arrayOfValues
case dictionary
}
extension JSONDecoder {
// Decodes an object by using a decoding strategy
func partialDecode<T>(_ type: T.Type, withStrategy strategy: PartialDecodingStrategy, from data: Data) throws -> T where T : PartiallyDecodable, T.PartialKeys.RawValue == String {
guard let partialJSON = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [AnyHashable:Any] else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Invalid JSON"))
}
guard let value = partialJSON["value"] else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Missing \"value\" key"))
}
let processedJSON: [AnyHashable:Any]
switch strategy {
case let .singleKey(key):
processedJSON = [key:value]
case .arrayOfValues:
guard let values = value as? [Any],
values.count == 2,
let properties = values[1] as? [Any] else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Invalid JSON: expected a 2 elements array for the \"value\" key"))
}
processedJSON = zip(T.PartialKeys.enumCases, properties)
.reduce(into: [:]) { $0[$1.0.rawValue] = $1.1 }
case .dictionary:
guard let dict = value as? [AnyHashable:Any] else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Invalid JSON: expected a dictionary for the \"value\" key"))
}
processedJSON = dict
}
return try decode(type, from: JSONSerialization.data(withJSONObject: processedJSON, options: []))
}
}
我们希望能够部分解码
Character
,因此我们让它采用所有必需的协议:
struct Character: Codable, PartiallyDecodable {
typealias PartialKeys = CodingKeys
let name: String?
let age: Int?
let hobbies: [String]?
enum CodingKeys: String, CodingKey {
case name
case age
case hobbies
}
}
现在是有趣的部分,让我们测试一下:
let decoder = JSONDecoder()
let jsonData1 = "{\"value\":\"Bilbo\"}".data(using: .utf8)!
print((try? decoder.partialDecode(Character.self,
withStrategy: .singleKey(Character.CodingKeys.name.rawValue),
from: jsonData1)) as Any)
let jsonData2 = "{\"value\":[\"[Ljava.lang.Object;\",[\"Bilbo\",111]]}".data(using: .utf8)!
print((try? decoder.partialDecode(Character.self,
withStrategy: .arrayOfValues,
from: jsonData2)) as Any)
let jsonData3 = "{\"value\":{\"name\":\"Bilbo\",\"age\":111,\"hobbies\":[\"rings\"]}}".data(using: .utf8)!
print((try? decoder.partialDecode(Character.self,
withStrategy: .dictionary,
from: jsonData3)) as Any)
正如我们所料,输出如下:
Optional(MyApp.Character(name: Optional("Bilbo"), age: nil, hobbies: nil))
Optional(MyApp.Character(name: Optional("Bilbo"), age: Optional(111), hobbies: nil))
Optional(MyApp.Character(name: Optional("Bilbo"), age: Optional(111), hobbies: Optional(["rings"])))
正如我们所看到的,在部署了适当的基础设施后,类型部分可解码的唯一要求是符合
PartiallyDecodable
并有一个枚举来说明要解码哪些键。这些要求很容易遵循。
我在这里使用了答案之一,直到我找到了这个很棒且更完整的解决方案: