当密钥仅在运行时已知时展平 JSON
Flattening JSON when keys are known only at runtime
假设我们有一个 JSON 结构,如下所示(常用于 Firebase 的实时数据库):
{
"18348b9b-9a49-4e04-ac35-37e38a8db1e2": {
"isActive": false,
"age": 29,
"company": "BALOOBA"
},
"20aca96e-663a-493c-8e9b-cb7b8272f817": {
"isActive": false,
"age": 39,
"company": "QUONATA"
},
"bd0c389b-2736-481a-9cf0-170600d36b6d": {
"isActive": false,
"age": 35,
"company": "EARTHMARK"
}
}
预期解决方案:
使用 Decodable
我想将其转换为包含 3 个元素的数组:
struct BoringEntity: Decodable {
let id: String
let isActive: Bool
let age: Int
let company: String
init(from decoder: Decoder) throws {
// ...
}
}
let entities: [BoringEntity] = try! JSONDecoder()...
id属性对应json对象的根字符串,例如:18348b9b-9a49-4e04-ac35-37e38a8db1e2
.
解决方法:
我已经尝试了几种方法,但在不需要辅助实体(或使用可选项)的情况下无法获得 id 属性:
/// Incomplete BoringEntity version to make Decodable conformance possible.
struct BoringEntityIncomplete: Decodable {
let isActive: Bool
let age: Int
let company: String
}
// Decode to aux struct
let decoded = try! JSONDecoder().decode([String : BoringEntityIncomplete].self, for: jsonData)
// Map aux entities to BoringEntity
let entities = decoded.map { BoringEntity(...) }
使用 init(from: Decoder)
并不像在其他情况下那么简单,因为 keyedContainer(,)
由于密钥未知而无法使用。
Decodable
不适合这些类型的情况吗?
在我回答你的问题之前有几件事:
1: 注释 (// id
) 使 JSON 无效。 JSON 不允许评论。
2:BoringEntity
中的id
属性从何而来?
struct BoringEntity: Decodable {
let id: String // where is it stored in the JSON???
let isActive: Bool
let age: Int
let company: String
}
如果我忽略了这些东西,您可以将 BoringEntity
的数组包装在结构 (BoringEntities
) 中。直接使用 [BoringEntity]
是不可取的,因为你必须掩盖 Array
.
的默认 init(from decoder:)
这里的技巧是让 JSONDecoder
通过 container.allKeys
属性:
返回键列表
struct BoringEntity: Decodable {
let isActive: Bool
let age: Int
let company: String
}
struct BoringEntities: Decodable {
var entities = [BoringEntity]()
// This really is just a stand-in to make the compiler happy.
// It doesn't actually do anything.
private struct PhantomKeys: CodingKey {
var intValue: Int?
var stringValue: String
init?(intValue: Int) { self.intValue = intValue; self.stringValue = "\(intValue)" }
init?(stringValue: String) { self.stringValue = stringValue }
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: PhantomKeys.self)
for key in container.allKeys {
let entity = try container.decode(BoringEntity.self, forKey: key)
entities.append(entity)
}
}
}
用法:
let jsonData = """
{
"18348b9b-9a49-4e04-ac35-37e38a8db1e2": {
"isActive": false,
"age": 29,
"company": "BALOOBA"
},
"20aca96e-663a-493c-8e9b-cb7b8272f817": {
"isActive": false,
"age": 39,
"company": "QUONATA"
},
"bd0c389b-2736-481a-9cf0-170600d36b6d": {
"isActive": false,
"age": 35,
"company": "EARTHMARK"
}
}
""".data(using: .utf8)!
let entities = try JSONDecoder().decode(BoringEntities.self, from: jsonData).entities
基础实体:
struct BoringEntity: Decodable {
let id: String
let isActive: Bool
let age: Int
let company: String
}
解决方案 1:使用不带密钥的额外结构
/// Incomplete BoringEntity version to make Decodable conformance possible.
private struct BoringEntityBare: Decodable {
let isActive: Bool
let age: Int
let company: String
}
// Decode to aux struct
private let decoded = try! JSONDecoder().decode([String : BoringEntityBare].self, from: jsonData)
// Map aux entities to BoringEntity
let entities = decoded.map { BoringEntity(id: [=11=].key, isActive: [=11=].value.isActive, age: [=11=].value.age, company: [=11=].value.company) }
print(entities)
解决方案 2:使用包装器
感谢 Code Different,我能够将我的方法与他的 PhantomKeys
想法结合起来,但没有办法解决它:必须始终使用额外的实体。
struct BoringEntities: Decodable {
var entities = [BoringEntity]()
// This really is just a stand-in to make the compiler happy.
// It doesn't actually do anything.
private struct PhantomKeys: CodingKey {
var intValue: Int?
var stringValue: String
init?(intValue: Int) { self.intValue = intValue; self.stringValue = "\(intValue)" }
init?(stringValue: String) { self.stringValue = stringValue }
}
private enum BareKeys: String, CodingKey {
case isActive, age, company
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: PhantomKeys.self)
// There's only one key
for key in container.allKeys {
let aux = try container.nestedContainer(keyedBy: BareKeys.self, forKey: key)
let age = try aux.decode(Int.self, forKey: .age)
let company = try aux.decode(String.self, forKey: .company)
let isActive = try aux.decode(Bool.self, forKey: .isActive)
let entity = BoringEntity(id: key.stringValue, isActive: isActive, age: age, company: company)
entities.append(entity)
}
}
}
let entities = try JSONDecoder().decode(BoringEntities.self, from: jsonData).entities
print(entities)
假设我们有一个 JSON 结构,如下所示(常用于 Firebase 的实时数据库):
{
"18348b9b-9a49-4e04-ac35-37e38a8db1e2": {
"isActive": false,
"age": 29,
"company": "BALOOBA"
},
"20aca96e-663a-493c-8e9b-cb7b8272f817": {
"isActive": false,
"age": 39,
"company": "QUONATA"
},
"bd0c389b-2736-481a-9cf0-170600d36b6d": {
"isActive": false,
"age": 35,
"company": "EARTHMARK"
}
}
预期解决方案:
使用 Decodable
我想将其转换为包含 3 个元素的数组:
struct BoringEntity: Decodable {
let id: String
let isActive: Bool
let age: Int
let company: String
init(from decoder: Decoder) throws {
// ...
}
}
let entities: [BoringEntity] = try! JSONDecoder()...
id属性对应json对象的根字符串,例如:18348b9b-9a49-4e04-ac35-37e38a8db1e2
.
解决方法:
我已经尝试了几种方法,但在不需要辅助实体(或使用可选项)的情况下无法获得 id 属性:
/// Incomplete BoringEntity version to make Decodable conformance possible.
struct BoringEntityIncomplete: Decodable {
let isActive: Bool
let age: Int
let company: String
}
// Decode to aux struct
let decoded = try! JSONDecoder().decode([String : BoringEntityIncomplete].self, for: jsonData)
// Map aux entities to BoringEntity
let entities = decoded.map { BoringEntity(...) }
使用 init(from: Decoder)
并不像在其他情况下那么简单,因为 keyedContainer(,)
由于密钥未知而无法使用。
Decodable
不适合这些类型的情况吗?
在我回答你的问题之前有几件事:
1: 注释 (// id
) 使 JSON 无效。 JSON 不允许评论。
2:BoringEntity
中的id
属性从何而来?
struct BoringEntity: Decodable {
let id: String // where is it stored in the JSON???
let isActive: Bool
let age: Int
let company: String
}
如果我忽略了这些东西,您可以将 BoringEntity
的数组包装在结构 (BoringEntities
) 中。直接使用 [BoringEntity]
是不可取的,因为你必须掩盖 Array
.
init(from decoder:)
这里的技巧是让 JSONDecoder
通过 container.allKeys
属性:
struct BoringEntity: Decodable {
let isActive: Bool
let age: Int
let company: String
}
struct BoringEntities: Decodable {
var entities = [BoringEntity]()
// This really is just a stand-in to make the compiler happy.
// It doesn't actually do anything.
private struct PhantomKeys: CodingKey {
var intValue: Int?
var stringValue: String
init?(intValue: Int) { self.intValue = intValue; self.stringValue = "\(intValue)" }
init?(stringValue: String) { self.stringValue = stringValue }
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: PhantomKeys.self)
for key in container.allKeys {
let entity = try container.decode(BoringEntity.self, forKey: key)
entities.append(entity)
}
}
}
用法:
let jsonData = """
{
"18348b9b-9a49-4e04-ac35-37e38a8db1e2": {
"isActive": false,
"age": 29,
"company": "BALOOBA"
},
"20aca96e-663a-493c-8e9b-cb7b8272f817": {
"isActive": false,
"age": 39,
"company": "QUONATA"
},
"bd0c389b-2736-481a-9cf0-170600d36b6d": {
"isActive": false,
"age": 35,
"company": "EARTHMARK"
}
}
""".data(using: .utf8)!
let entities = try JSONDecoder().decode(BoringEntities.self, from: jsonData).entities
基础实体:
struct BoringEntity: Decodable {
let id: String
let isActive: Bool
let age: Int
let company: String
}
解决方案 1:使用不带密钥的额外结构
/// Incomplete BoringEntity version to make Decodable conformance possible.
private struct BoringEntityBare: Decodable {
let isActive: Bool
let age: Int
let company: String
}
// Decode to aux struct
private let decoded = try! JSONDecoder().decode([String : BoringEntityBare].self, from: jsonData)
// Map aux entities to BoringEntity
let entities = decoded.map { BoringEntity(id: [=11=].key, isActive: [=11=].value.isActive, age: [=11=].value.age, company: [=11=].value.company) }
print(entities)
解决方案 2:使用包装器
感谢 Code Different,我能够将我的方法与他的 PhantomKeys
想法结合起来,但没有办法解决它:必须始终使用额外的实体。
struct BoringEntities: Decodable {
var entities = [BoringEntity]()
// This really is just a stand-in to make the compiler happy.
// It doesn't actually do anything.
private struct PhantomKeys: CodingKey {
var intValue: Int?
var stringValue: String
init?(intValue: Int) { self.intValue = intValue; self.stringValue = "\(intValue)" }
init?(stringValue: String) { self.stringValue = stringValue }
}
private enum BareKeys: String, CodingKey {
case isActive, age, company
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: PhantomKeys.self)
// There's only one key
for key in container.allKeys {
let aux = try container.nestedContainer(keyedBy: BareKeys.self, forKey: key)
let age = try aux.decode(Int.self, forKey: .age)
let company = try aux.decode(String.self, forKey: .company)
let isActive = try aux.decode(Bool.self, forKey: .isActive)
let entity = BoringEntity(id: key.stringValue, isActive: isActive, age: age, company: company)
entities.append(entity)
}
}
}
let entities = try JSONDecoder().decode(BoringEntities.self, from: jsonData).entities
print(entities)