使用 swift Codable 以值作为键来解码 JSON
Posted
技术标签:
【中文标题】使用 swift Codable 以值作为键来解码 JSON【英文标题】:Use swift Codable to decode JSON with values as keys 【发布时间】:2019-01-10 13:23:37 【问题描述】:我在解码 JSON 结构时遇到问题,我无法更改它以使其更易于解码(它来自 firebase)..
如何将以下 JSON 解码为对象? 问题是如何转换“7E7-M001”。这是一个有抽屉的容器的名称。抽屉名称也用作键。
"7E7-M001" :
"Drawer1" :
"101" :
"Partnumber" : "F101"
,
"102" :
"Partnumber" : "F121"
,
"7E7-M002":
"Drawer1":
"201":
"Partnumber": "F201"
,
"202":
"Partnumber": "F221"
我必须在 Container & Drawer 类中修复什么以将键作为标题属性和这些类中的对象数组?
class Container: Codable
var title: String
var drawers: [Drawer]
class Drawer: Codable
var title: String
var tools: [Tool]
class Tool: Codable
var title: String
var partNumber: String
enum CodingKeys: String, CodingKey
case partNumber = "Partnumber"
【问题讨论】:
【参考方案1】:首先,我将做一些简单的简化,以便我可以专注于这个问题的重点。我要让一切都不可变,用结构替换类,并且只实现 Decodable。使这个 Encodable 是一个单独的问题。
处理未知值键的核心工具是可以处理任何字符串的 CodingKey:
struct TitleKey: CodingKey
let stringValue: String
init?(stringValue: String) self.stringValue = stringValue
var intValue: Int? return nil
init?(intValue: Int) return nil
第二个重要的工具是知道自己的头衔的能力。这意味着询问解码器“我们在哪里?”这是当前编码路径中的最后一个元素。
extension Decoder
func currentTitle() throws -> String
guard let titleKey = codingPath.last as? TitleKey else
throw DecodingError.dataCorrupted(.init(codingPath: codingPath,
debugDescription: "Not in titled container"))
return titleKey.stringValue
然后我们需要一种方法来解码以这种方式“命名”的元素:
extension Decoder
func decodeTitledElements<Element: Decodable>(_ type: Element.Type) throws -> [Element]
let titles = try container(keyedBy: TitleKey.self)
return try titles.allKeys.map title in
return try titles.decode(Element.self, forKey: title)
这样,我们可以为这些“有标题”的东西发明一个协议并对其进行解码:
protocol TitleDecodable: Decodable
associatedtype Element: Decodable
init(title: String, elements: [Element])
extension TitleDecodable
init(from decoder: Decoder) throws
self.init(title: try decoder.currentTitle(),
elements: try decoder.decodeTitledElements(Element.self))
这就是大部分工作。我们可以使用这个协议使上层的解码变得非常容易。只需实现init(title:elements:)
。
struct Drawer: TitleDecodable
let title: String
let tools: [Tool]
init(title: String, elements: [Tool])
self.title = title
self.tools = elements
struct Container: TitleDecodable
let title: String
let drawers: [Drawer]
init(title: String, elements: [Drawer])
self.title = title
self.drawers = elements
Tool
有点不同,因为它是一个叶节点,还有其他东西要解码。
struct Tool: Decodable
let title: String
let partNumber: String
enum CodingKeys: String, CodingKey
case partNumber = "Partnumber"
init(from decoder: Decoder) throws
self.title = try decoder.currentTitle()
let container = try decoder.container(keyedBy: CodingKeys.self)
self.partNumber = try container.decode(String.self, forKey: .partNumber)
只剩下最顶层。我们将创建一个 Containers
类型,只是为了结束。
struct Containers: Decodable
let containers: [Container]
init(from decoder: Decoder) throws
self.containers = try decoder.decodeTitledElements(Container.self)
要使用它,解码***Containers
:
let containers = try JSONDecoder().decode(Containers.self, from: json)
print(containers.containers)
请注意,由于 JSON 对象不是保序的,因此数组的顺序可能与 JSON 不同,并且在运行之间的顺序也可能不同。
Gist
【讨论】:
难以置信的答案 See below 我已经扩展了 Rob 的答案,以提供更一般的答案并赋予它更多功能。【参考方案2】:我将扩展 Rob 的答案,以提供更一般的答案并赋予它更多功能。首先,我们将举一个 Json 示例,并确定其中可以包含的所有场景。
let json = Data("""
"id": "123456", // id -> primitive data type that can be decoded normally
"name": "Example Name", // name -> primitive data type that can be decoded
"address": // address -> key => static, object => has static key-value pairs
"city": "Negombo",
"country": "Sri Lanka"
,
"email": // email -> key => static, object => has only one key-value pair which has a dynamic key. When you're sure, user can have only one email.
"example@gmail.com": // example@gmail.com -> key => dynamic key, object => in this example the object is
// normal decodable object. But you can have objects that has dynamic key-value pairs.
"verified": true
,
"phone_numbers": // phone_numbers -> key => static, object => has multiple key-value pairs which has a dynamic keys. Assume user can have multiple phone numbers.
"+94772222222": // +94772222222 -> key => dynamic key, object => in this example the object is
// normal decodable object. But you can have objects that has dynamic key-value pairs.
"isActive": true
,
"+94772222223": // +94772222223 -> key => another dynamic key, object => another object mapped to dynamic key +94772222223
"isActive": false
""".utf8)
最后,您将能够读取所有值,如下所示,
let decoder = JSONDecoder()
do
let userObject = try decoder.decode(UserModel.self, from: json)
print("User ID : \(String(describing: userObject.id))")
print("User Name : \(String(describing: userObject.name))")
print("User Address city : \(String(describing: userObject.address?.city))")
print("User Address country: \(String(describing: userObject.address?.country))")
print("User Email. : \(String(describing: userObject.email?.emailContent?.emailAddress))")
print("User Email Verified : \(String(describing: userObject.email?.emailContent?.verified))")
print("User Phone Number 1 : \(String(describing: userObject.phoneNumberDetails?.phoneNumbers.first?.number))")
print("User Phone Number 2 : \(String(describing: userObject.phoneNumberDetails?.phoneNumbers[1].number))")
print("User Phone Number 1 is Active : \(String(describing: userObject.phoneNumberDetails?.phoneNumbers.first?.isActive))")
print("User Phone Number 2 is Active : \(String(describing: userObject.phoneNumberDetails?.phoneNumbers[1].isActive))")
catch
print("Error deserializing JSON: \(error)")
所以最多地址键,你可以很容易地解码。但在那之后,您将需要一个特定的 Object 结构来保存由动态键值对映射的所有数据。 所以这是我建议的 Swift 对象结构。假设上面的 Json 是用于 UserModel。
import Foundation
struct UserModel: Decodable
let id: String
let name: String
let address: Address?
let email: Email?
let phoneNumberDetails: PhoneNumberDetails?
enum CodingKeys: String, CodingKey
case id
case name
case address
init(from decoder: Decoder) throws
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(String.self, forKey: .id)
self.name = try container.decode(String.self, forKey: .name)
self.address = try? container.decode(Address.self, forKey: .address)
// ["email": Value] -> static key => Email Swift Object
// ["email": Value] -> only object => email.emailContent. Here Value has only one object.
self.email = try decoder.decodeStaticTitledElement(with: TitleKey(stringValue: "email")!, Email.self)
// ["phone_numbers": Value] -> static key => PhoneNumberDetails Swift Object
// ["phone_numbers": Value] -> multiple objects => phoneNumberDetails.phoneNumbers. Here Value has multiples objects.
self.phoneNumberDetails = try decoder.decodeStaticTitledElement(with: TitleKey(stringValue: "phone_numbers")!, PhoneNumberDetails.self)
struct Address: Decodable
let city: String
let country: String
enum CodingKeys: String, CodingKey
case city
case country
init(from decoder: Decoder) throws
let container = try decoder.container(keyedBy: CodingKeys.self)
self.city = try container.decode(String.self, forKey: .city)
self.country = try container.decode(String.self, forKey: .country)
/*
* Extends SingleTitleDecodable.
* Object that was mapped to static key "email".
* SingleTitleDecodable uses when you know the Parent object has only one dynamic key-value pair
* In this case Parent object is "email" object in the json, and "example@gmail.com": body is the only dynamic key-value pair
* key-value pair is mapped into EmailContent
*/
struct Email: SingleTitleDecodable
let emailContent: EmailContent?
init(title: String, element: EmailContent?)
self.emailContent = element
struct EmailContent: Decodable
let emailAddress: String
let verified: Bool
enum CodingKeys: String, CodingKey
case verified
init(from decoder: Decoder) throws
self.emailAddress = try decoder.currentTitle()
let container = try decoder.container(keyedBy: CodingKeys.self)
self.verified = try container.decode(Bool.self, forKey: .verified)
/*
* Extends TitleDecodable.
* Object that was mapped to static key "phone_numbers".
* TitleDecodable uses when you know the Parent object has multiple dynamic key-value pair
* In this case Parent object is "phone_numbers" object in the json, and "+94772222222": body , "+94772222222": body are the multiple dynamic key-value pairs
* Multiple dynamic key-value pair are mapped into PhoneNumber array
*/
struct PhoneNumberDetails: TitleDecodable
let phoneNumbers: [PhoneNumber]
init(title: String, elements: [PhoneNumber])
self.phoneNumbers = elements
struct PhoneNumber: Decodable
let number: String
let isActive: Bool
enum CodingKeys: String, CodingKey
case isActive
init(from decoder: Decoder) throws
self.number = try decoder.currentTitle()
let container = try decoder.container(keyedBy: CodingKeys.self)
self.isActive = try container.decode(Bool.self, forKey: .isActive)
关注 Json 是如何转化为 Object 结构的。这是从 Rob 的答案中提取和改进的机制。
import Foundation
/*
* This is to handle unknown keys.
* Convert Keys with any String value to CodingKeys
*/
struct TitleKey: CodingKey
let stringValue: String
init?(stringValue: String) self.stringValue = stringValue
var intValue: Int? return nil
init?(intValue: Int) return nil
extension Decoder
/*
* Decode map into object array that is type of Element
* [Key: Element] -> [Element]
* This will be used when the keys are dynamic and have multiple keys
* Within type Element we can embed relevant Key using => 'try decoder.currentTitle()'
* So you can access Key using => 'element.key'
*/
func decodeMultipleDynamicTitledElements<Element: Decodable>(_ type: Element.Type) throws -> [Element]
var decodables: [Element] = []
let titles = try container(keyedBy: TitleKey.self)
for title in titles.allKeys
if let element = try? titles.decode(Element.self, forKey: title)
decodables.append(element)
return decodables
/*
* Decode map into optional object that is type of Element
* [Key: Element] -> Element?
* This will be used when the keys are dynamic and when you're sure there'll be only one key-value pair
* Within type Element we can embed relevant Key using => 'try decoder.currentTitle()'
* So you can access Key using => 'element.key'
*/
func decodeSingleDynamicTitledElement<Element: Decodable>(_ type: Element.Type) throws -> Element?
let titles = try container(keyedBy: TitleKey.self)
for title in titles.allKeys
if let element = try? titles.decode(Element.self, forKey: title)
return element
return nil
/*
* Decode map key-value pair into optional object that is type of Element
* Key: Element -> Element?
* This will be used when the root key is known, But the value is constructed with Maps where the keys can be Unknown
*/
func decodeStaticTitledElement<Element: Decodable>(with key: TitleKey, _ type: Element.Type) throws -> Element?
let titles = try container(keyedBy: TitleKey.self)
if let element = try? titles.decode(Element.self, forKey: key)
return element
return nil
/*
* This will be used to know where the Element is in the Object tree
* Returns the Key of the Element which was mapped to
*/
func currentTitle() throws -> String
guard let titleKey = codingPath.last as? TitleKey else
throw DecodingError.dataCorrupted(.init(codingPath: codingPath, debugDescription: "Not in titled container"))
return titleKey.stringValue
/*
* Class that implements this Protocol, contains an array of Element Objects,
* that will be mapped from a 'Key1: [Key2: Element]' type of map.
* This will be used when the Key2 is dynamic and have multiple Key2 values
* Key1 -> Key1: TitleDecodable
* [Key2: Element] -> Key1_instance.elements
* Key2 -> Key1_instance.elements[index].key2
*/
protocol TitleDecodable: Decodable
associatedtype Element: Decodable
init(title: String, elements: [Element])
extension TitleDecodable
init(from decoder: Decoder) throws
self.init(title: try decoder.currentTitle(), elements: try decoder.decodeMultipleDynamicTitledElements(Element.self))
/*
* Class that implements this Protocol, contains a variable which is type of Element,
* that will be mapped from a 'Key1: [Key2: Element]' type of map.
* This will be used when the Keys2 is dynamic and have only one Key2-value pair
* Key1 -> Key1: SingleTitleDecodable
* [Key2: Element] -> Key1_instance.element
* Key2 -> Key1_instance.element.key2
*/
protocol SingleTitleDecodable: Decodable
associatedtype Element: Decodable
init(title: String, element: Element?)
extension SingleTitleDecodable
init(from decoder: Decoder) throws
self.init(title: try decoder.currentTitle(), element: try decoder.decodeSingleDynamicTitledElement(Element.self))
【讨论】:
【参考方案3】:在这种情况下,我们无法为此 JSON 创建静态 codable
类。
最好使用JSON serialization
并检索它。
【讨论】:
您可以使用Codable
创建类,但您必须编写自定义初始化程序。以上是关于使用 swift Codable 以值作为键来解码 JSON的主要内容,如果未能解决你的问题,请参考以下文章
Swift Codable 将空 json 解码为 nil 或空对象
Swift Codable 期望解码 Dictionary<String, Any> 但找到了一个字符串/数据