简体   繁体   English

使用 Codable 解析嵌套的无键 JSON

[英]Parsing nested unkeyed JSON with Codable

I am trying to parse an array of heterogeneous objects using Codable.我正在尝试使用 Codable 解析一组异构对象。 These objects are also unkeyed as well.这些对象也没有键控。 I should note that I have the container structure correct, because it DOES loop through and print "it is type1" at all correct times as seen below.我应该注意我的容器结构是正确的,因为它确实循环并在所有正确的时间打印“它是 type1”,如下所示。 I just can't figure out how to access the actual object.我只是不知道如何访问实际的 object。 Here is my code:这是我的代码:

var data: [Any]

public init(from decoder: Decoder) throws {
    var container = try! decoder.container(keyedBy: CodingKeys.self).nestedUnkeyedContainer(forKey: .data)

    while !container.isAtEnd {
        var itemContainer = try container.nestedContainer(keyedBy: CodingKeys.self)

        let itemType = try itemContainer.decode(String.self, forKey: .type)
        switch itemType {
        case "type1":
            print("it is type1")
            // this does not compile, but is what I need
            //let objectOfItem1 = try itemContainer.decode(Type1.self)

            // this compiles, but doesn't work because there is no key with these objects
            //let objectOfItem1 = try itemContainer.decode(Type1, forKey: .type)
        default:
            print("test:: it is the default")
        }
    }
}

private enum CodingKeys: String, CodingKey {
    case data
    case type
}

And here is the JSON I am trying to decode (many properties committed for clarity):这是我正在尝试解码的 JSON (为清楚起见,提交了许多属性):

"contents" : {
      "data" : [
        {
          "type" : "type1",
          "id" : "6a406cdd7a9cace5"
        }, 
       {
          "type" : "type2",
          "id" : "ljhdgsouilghoipsu"
        }
     ]
 }

How can I correctly get my individual Type1 objects out of this structure?如何正确地将我的单个Type1对象从这个结构中取出?

I think the easy way to get around the heterogenous data is to use an enum as an interim type to wrap your various Item types (which all need to Codable):我认为绕过异构数据的简单方法是使用枚举作为临时类型来包装各种 Item 类型(都需要 Codable):

To allow myself to test this I've changed your json slightly to give me more heterogenous data for testing.为了让自己对此进行测试,我稍微更改了您的 json,以便为我提供更多异构数据进行测试。 I've used:我用过:

let json = """
{
  "contents": {
    "data": [
      {
        "type": "type1",
        "id": "6a406cdd7a9cace5"
      },
      {
        "type": "type2",
        "dbl": 1.01
      },
      {
        "type": "type3",
        "int": 5
      }
    ]
  }
}

and then created the three final types represented by this json然后创建了这个 json 代表的三种最终类型

struct Item1: Codable {
   let type: String
   let id: String
}
struct Item2: Codable {
   let type: String
   let dbl: Double
}
struct Item3: Codable {
   let type: String
   let int: Int
}

To allow decoding the multiple types in a type-safe way (as required by Codable ) you need to use a single type that can represent (or wrap) the possible options.为了允许以类型安全的方式解码多种类型(根据Codable的要求),您需要使用可以表示(或包装)可能选项的单一类型。 An enum with associated values works nicely for this具有关联值的枚举很好地为此工作

enum Interim {
  case type1 (Item1)
  case type2 (Item2)
  case type3 (Item3)
  case unknown  //to handle unexpected json structures
}

So far, so good, but then it gets slightly more complicated when it comes to creating the Interim from the JSON.到目前为止,一切都很好,但是从 JSON 创建Interim版本时会稍微复杂一些。 It will need a CodingKey enum which represents all the possible keys for all the Item# types, and then it will need to decode the JSON linking all these keys to their respective types and data:它需要一个CodingKey枚举,它代表所有Item#类型的所有可能键,然后它需要解码 JSON 将所有这些键链接到它们各自的类型和数据:

extension Interim: Decodable {
   private enum InterimKeys: String, CodingKey {
      case type
      case id
      case dbl
      case int
   }
   init(from decoder: Decoder) throws {
      let container = try decoder.container(keyedBy: InterimKeys.self)
      let type = try container.decode(String.self, forKey: .type)
      switch type {
      case "type1":
         let id = try container.decode(String.self, forKey: .id)
         let item = Item1(type: type, id: id)
         self = .type1(item)
      case "type2":
         let dbl = try container.decode(Double.self, forKey: .dbl)
         let item = Item2(type: type, dbl: dbl)
         self = .type2(item)
      case "type3":
         let int = try container.decode(Int.self, forKey: .int)
         let item = Item3(type: type, int: int)
         self = .type3(item)
      default: self = .unknown
      }
   }
}

This provides the mechanism for decoding the heterogenous components, now we just need to deal with the higher-level keys.这提供了解码异构组件的机制,现在我们只需要处理更高级别的密钥。 As we have a Decodable Interim type this is straightforward:由于我们有一个可DecodableInterim类型,这很简单:

struct DataArray: Decodable {
   var data: [Interim]
}

struct Contents: Decodable {
   var contents: DataArray
}

This now means the json can be decoded like this...现在这意味着 json 可以像这样解码......

let data = Data(json.utf8)
let decoder = JSONDecoder()
do {
    let contents = try decoder.decode(Contents.self, from: data)
    print(contents)
} catch {
    print("Failed to decode JSON")
    print(error.localizedDescription)
}

This successfully decodes the data into a nested structure where the major component is the array of Interim types with their associated Item# objects.这成功地将数据解码为嵌套结构,其中主要组件是Interim类型数组及其关联的Item#对象。 The above produces the following output, showing these nested types:上面生成了以下 output,显示了这些嵌套类型:

Contents(contents: testbed.DataArray(data: [testbed.Interim.type1(testbed.Item1(type: "type1", id: "6a406cdd7a9cace5")), testbed.Interim.type2(testbed.Item2(type: "type2", dbl: 1.01)), testbed.Interim.type3(testbed.Item3(type: "type3", int: 5))]))

I think there should be an even safer way to do this with Type Erasure to provide a more extensible solution, but I've not got my head around that fully yet.我认为应该有一种更安全的方法来使用 Type Erasure 来提供更可扩展的解决方案,但我还没有完全理解这一点。

I think you need use this structure:我认为你需要使用这个结构:

struct A: Codable {
let contents: B?

enum CodingKeys: String, CodingKey {
    case contents
}

struct B: Codable {
    let data: [C]?

    enum CodingKeys: String, CodingKey {
        case data
    }

    struct C: Codable {
        let type : String?
        let id : String?
    }

}

} }

extension A {

init(from decoder: Decoder) throws {

    let container = try decoder.container(keyedBy: CodingKeys.self)

    let contents = try container.decodeIfPresent(B.self, forKey: .contents)

    self.init(contents: contents)
}

} }

I'd like to add to flanker's answer an improvement for cleaner approach to avoid having all possible keys to be stored under Interim 's CodingKey .我想在侧卫的回答中添加一个改进,以改进更清洁的方法,以避免将所有可能的密钥存储在InterimCodingKey下。 Here is an updated Interim这是一个更新的Interim

enum Interim: Decodable {

    case item1(Item1)
    case item2(Item2)
    case item3(Item3)
    case unknown

    init(from decoder: Decoder) throws {
        let typeContainer = try decoder.container(keyedBy: Key.self)

        // Fallback for any unsupported types
        guard let type = try? typeContainer.decode(ItemType.self, forKey: .type) else {
            self = .unknown
            return
        }

        // Let corresponding Decodable Item to be initialized from the same decoder.
        switch type {
            case .type1: self = .item1(try .init(from: decoder))
            case .type2: self = .item2(try .init(from: decoder))
            case .type3: self = .item3(try .init(from: decoder))
        }
    }

    /// These are values for Item.type
    private enum ItemType: String, Decodable {
        case type1
        case type2
        case type3
    }

    private enum Key: CodingKey {
        case type
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM