使用Swift Codable解码以值作为键的JSON

时间:2019-01-10 13:23:37

标签: json swift codable

我在解码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类中解决哪些问题,才能将键作为title属性和这些类中的对象数组?

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"
    }
}

4 个答案:

答案 0 :(得分:8)

首先,我将略作简化,以便我可以集中讨论此问题的重点。我将使所有内容不变,用结构替换类,仅实现Decodable。使其成为可编码状态是一个单独的问题。

用于处理未知值键的中央工具是CodingKey,它可以处理任何字符串:

a-b

第二个重要工具是知道自己的标题的能力。这意味着询问解码器“我们在哪里?”那是当前编码路径中的最后一个元素。

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

请注意,由于JSON对象不是按顺序保留的,因此数组可能与JSON的顺序不同,并且两次运行之间的顺序也可能不同。

Gist

答案 1 :(得分: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 Object结构。假设上面的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结构。这是从罗伯的答案中提取和改进的机制。

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))
    }
}

答案 2 :(得分:0)

在这种情况下,我们无法为此 JSON 创建静态a | a == a类。 最好使用const API_publicKey = " "; function payWithRave() { var x = getpaidSetup({ PBFPubKey: API_publicKey, customer_email: "wwackuaku@yahoo.com", amount: 0, customer_phone: "233244631868", currency: "GHS", country: "GH", payment_options: "card", custom_logo: "https://ananseman.com/assets/images/masks.png", txref: "rave-1543925647", meta: [{ metaname: "GHsupportID", metavalue: "SP1234" }], onclose: function() {}, callback: function(response) { var txref = response.tx.txRef; // collect txRef returned and pass to a server page to complete status check. console.log("This is the response returned after a charge", response); if ( response.tx.chargeResponseCode == "00" || response.tx.chargeResponseCode == "0" ) { // redirect to a success page } else { // redirect to a failure page. } x.close(); // use this to close the modal immediately after payment. } }); } </script>``` 并进行检索。

答案 3 :(得分:0)

我从类更改为结构,并使用了字典,并且如您所见,一个结构(类)已消失,我暂时忽略了CodingKeys,但您可能想要添加它。您的title属性现在是字典中的键。

struct Container: Decodable {
    var Drawer1:  [String: Tool]
}

struct  Tool: Decodable {
    var Partnumber: String
}

示例

let decoder = JSONDecoder()

do {
    let result = try decoder.decode([String: Container].self, from: json)
    for x in result {
        let drawer = x.value.Drawer1
        //...
   }
} catch {
    print(error)
}

对我来说,如果使用类而不是struct,效果也一样好,但这也许是因为我仅实现json的解码