一尘不染

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

swift

我在解码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"
    }
}

阅读 244

收藏
2020-07-07

共1个答案

一尘不染

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

处理未知值键的中心工具是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的顺序不同,并且两次运行之间的顺序也可能不同。

要旨

2020-07-07