Using Codable to decode different classes using the same key

好久不见. 提交于 2019-12-23 06:05:10

问题


I'm working with an API that provides 2 JSON URLS. Each URL contains a nested container with different attributes that belong to the same class and object.

JSON URL 1

{
  "last_updated": 1535936629,
  "xyz": 5,
  "data": {
    "dataList": [
      {
        "id": "42",
        "a1": "a1value",
        "a2": "a2value",
      },
      // ,,,
    ]
  }
}

JSON URL 2

{
  "last_updated": 1536639996,
  "xyz": 5,
  "data": {
    "dataList": [
      {
        "id": "42",
        "a3": "a3value",
        "a4": "a4value",
      },
      // ,,,
    ]
  }
}

I want to use these JSON URLS to create a single Codable CustomClass object using the items in the nested dataList list, so I created a Feed struct to handle these 2 JSON files.

Feed.swift

import Foundation

Struct Feed: Decodable {
  var lastUpdated: Int
  var xyz: Int
  var data: KeyedDecodingContainer<Feed.dataCodingKey>
  var dataList: [CustomClass]

  enum CodingKeys: String, CodingKey {
    case lastUpdated = "last_updated"
    case xyz
    case data
  }

  enum dataCodingKey: String, CodingKey {
    case dataList
  }

  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.lastUpdated = try decoder.decode(Int.self, forKey: .lastUpdated)
    self.xyz = try container.decode(Int.self, forKey: .xyz)
    self.data = try container.nestedContainer(keyedBy: dataCodingKey.self, forKey: .data)
    self.dataList = try data.decode([CustomClass].self, forKey: .dataList)
  }
}

CustomClass.swift

class CustomClass: Decodable {

    var id: String
    var a1: String
    var a2: Double
    var a3: String
    var a4: String

    enum CodingKeys: String, CodingKey {
        case id
        case a1
        case a2
        case a3
        case a4
    }

    required init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        self.id = try values.decode(String.self, forKey: .id)
        self.a1 = try values.decode(String.self, forKey: .a1)
        self.a2 = try values.decode(String.self, forKey: .a2)
        self.a3 = try values.decode(String.self, forKey: .a3)
        self.a4 = try values.decode(String.self, forKey: .a4)
    }
}

In my ViewController, I do two separate asynchronous calls to obtain the data:

ViewController.swift

var listOfCustomClass: [CustomClass]
var listOfCustomClass2: [CustomClass]

func getFeed(urlString: String, completionHandler: @escaping (_ result: Feed?) -> Void) {
    // parses JSON to return a Feed object

    guard let url = URL(string: urlString) else { return }
    URLSession.shared.dataTask(with: url) { (data, response, error) in
        if error != nil {
            print(error!)
        }
        guard let data = data else { return }

        //Implement JSON decoding and parsing
         do {
            let feed = try JSONDecoder().decode(Feed.self, from: data)
            DispatchQueue.main.async {
                completionHandler(feed)
            }
        } catch {
            print(error)
        }
    }.resume()
}

getFeed(urlString: url1String) { result in
  // Obtain the contents of dataList from URL1
  if let feed = result {
    self.listOfCustomClass = feed.dataList
    self.getFeed(urlString: url2String) { result in
      //Upon completion, obtain the dataList info and populate the "a3" and "a4" attributes from CustomClass

      if let feed = result {
        let dataList2: [CustomClass] = feed.dataList

      // code to merge self.listOfCustomClass 1 and self.listOfCustomClass2 into a single [CustomClass] list with all attributes and store it as self.listOfCustomClass   

        // Upon completion, return the finalized station array for use
        DispatchQueue.main.async {
          completionHandler(self.listOfCustomClass)
        }
      }
    }
  }
}

The problem I'm running into is that the dataList CodingKey has different keys a1 or a2 if coming from URL1 or a3, a4 if coming from URL2. Therefore the Codable init method is complaining whenever it can't find 2 of 4 keys in the dataList container.

How can I approach creating one CustomClass object with a1, a2, a3, and a4 instantiated using a single Decoder?


回答1:


If the JSON to CustomClass may or may or may not contain keys a1, a2, etc, then they must be optional…

let a1: String?
let a2: Double?
let a3: String?
let a4: String?

Then it's just a case of using

a1 = try values.decodeIfPresent(String.self, forKey: .a1)



回答2:


My suggestion is to use generics. Pass the type of the dataList object as generic type in Feed. You can even decode lastUpdated to Date with the appropriate dateDecodingStrategy

struct Feed<T : Decodable>: Decodable {
    let lastUpdated: Date
    let xyz: Int
    let data: DataList<T>
}

struct DataList<T : Decodable> : Decodable {
    let dataList: [T]
}

The type of the dataList object can be anything which conforms to Decodable, the given JSON can be decoded to these two structs or classes:

class CustomClass1 : Decodable {
    let id, a1, a2: String
}

class CustomClass2 : Decodable {
    let id, a3, a4: String
}

The benefit of multiple types is the complete avoiding of any key and type checking.

For example to decode the first JSON write

let json = """
{
    "last_updated": 1535936629,
    "xyz": 5,
    "data": {
        "dataList": [{"id": "42", "a1": "a1value", "a2": "a2value"}]
    }
}
"""

let data = Data(json.utf8)
do {
    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    decoder.dateDecodingStrategy = .secondsSince1970
    let result = try decoder.decode(Feed<CustomClass1>.self, from: data)
    print(result)
} catch {
    print(error)
}


来源:https://stackoverflow.com/questions/52342229/using-codable-to-decode-different-classes-using-the-same-key

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!