0

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?

7
  • It's not at all clear what you're asking here. Please read minimal reproducible example and then edit your question showing the minimum code that demonstrates the problem Commented Sep 15, 2018 at 14:51
  • And please give an actual example of code that's not working. e.g if your class inherits from MKAnnotation, show that in your question - but don't include all the code from your view controller - how the data is downloaded doesn't effect how the data is decoded. Commented Sep 15, 2018 at 15:07
  • @AshleyMills I've edited my question to include the incremental info I provided in the comments, and clarified the question in the last sentence. Commented Sep 15, 2018 at 15:50
  • Changing your question after you've received answers is inappropriate, as it invalidates the answers you've received. It can even make those answers wrong, and adversely affect the reputation of those who answered. If you now have a new or additional question, create a new post and ask it there; you can link back to this one if needed for reference. I've rolled back your question to correspond to the answers given. Commented Sep 15, 2018 at 15:53
  • @AshleyMills clearly I'm not doing things right here. I took it from your previous response that it wasn't clear what the question was, and that adding incremental information to the comments was bad practice, and that you requested that I edit the question body itself. I apologize for the back and forth. I'm unsure how to properly receive feedback at this point. Commented Sep 15, 2018 at 16:16

2 Answers 2

5

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)
}
Sign up to request clarification or add additional context in comments.

3 Comments

This should be the accepeted answer
How do you want understand each which type of class should be decoded?
The type is specified in the generic type CustomClass1 in Feed<CustomClass1>.
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)

4 Comments

The problem with this solution is that I plan to make CustomClass inherit MKAnnotation, which requires a non-optional initialization of a coordinate var.
In that case, include a coordinate var that is non optional - in which case it must be present in the JSON
Unfortunately, the JSON only includes a latitude var and a longitude var, and not a coordinate var.
OK, so you decode latitude and longitude and create a coordinate object from both in your init method. This wasn't included in your original question though. Without all the information it's hard to give an answer, especially if the goalposts keep changing.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.