CodeGeass
CodeGeass

Reputation: 639

Decoding dynamic JSON structure in swift 4

I have the following issue that I'm not sure how to handle.

My JSON response can look like this:

{ 
  "data": {
      "id": 7,
      "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NDY1MTU0NDMsImRhdGEiOiJ2bGFkVGVzdCIsImlhdCI6MTU0NjUwODI0M30.uwuPhlnchgBG4E8IvHvK4bB1Yj-TNDgmi7wUAiKmoVo"
   },
  "error": null
}

Or like this:

{
 "data": [{
     "id": 12
    }, {
      "id": 2
    }, {
       "id": 5
    }, {
       "id": 7
    }],
 "error": null
}

So in short the data can be either a single objet or an Array. What i have is this:

struct ApiData: Decodable {
    var data: DataObject?
    var error: String?
}

struct DataObject: Decodable {
    var userId: Int?

    enum CodingKeys: String, CodingKey {
        case userId = "id"
    }
}

This works fine for the first use case, but it will fail once data turns into

var data: [DataObject?]

How do I make that dynamic without duplicating code?

Edit: This is how i decode the object as well

 func makeDataTaskWith(with urlRequest: URLRequest, completion: @escaping(_ apiData: ApiData?) -> ()) {
    let config = URLSessionConfiguration.default
    let session = URLSession(configuration: config)

    session.dataTask(with: urlRequest) {
        (data, response, error) in
        guard let _ = response, let data = data else {return}

        if let responseCode = response as? HTTPURLResponse {
            print("Response has status code: \(responseCode.statusCode)")
        }

        do {
            let retreived = try NetworkManager.shared.decoder.decode(ApiData.self, from: data)
            completion(retreived)
        } catch let decodeError as NSError {
            print("Decoder error: \(decodeError.localizedDescription)\n")
            return
        }
        }.resume()
}

Upvotes: 0

Views: 1395

Answers (4)

SPatel
SPatel

Reputation: 4946

Using power of generic, it simple like below:

struct ApiData<T: Decodable>: Decodable {
    var data: T?
    var error: String?
}

struct DataObject: Decodable {
    private var id: Int?

    var userId:Int? {
        return id
    }
}

Use

if let obj = try? NetworkManager.shared.decoder.decode(ApiData<DataObject>.self, from: data) {
    //Do somthing
} else if let array = try NetworkManager.shared.decoder.decode(ApiData<[DataObject]>.self, from: data) {
    // Do somthing
}

Upvotes: 2

vadian
vadian

Reputation: 285069

If data can be a single object or an array write a custom initializer which decodes first an array, if a type mismatch error occurs decode a single object. data is declared as an array anyway.

As token appears only in a single object the property is declared as optional.

struct ApiData: Decodable {
    let data : [DataObject]
    let error : String?

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

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        do {
            data = try container.decode([DataObject].self, forKey: .data)
        } catch DecodingError.typeMismatch {
            data = [try container.decode(DataObject.self, forKey: .data)]
        }
        error = try container.decodeIfPresent(String.self, forKey: .error)
    }
}


struct DataObject: Decodable {
    let userId : Int
    let token : String?

    private enum CodingKeys: String, CodingKey { case userId = "id", token }
}

Edit: Your code to receive the data can be improved. You should add a better error handling to return also all possible errors:

func makeDataTaskWith(with urlRequest: URLRequest, completion: @escaping(ApiData?, Error?) -> Void) {
    let config = URLSessionConfiguration.default
    let session = URLSession(configuration: config)

    session.dataTask(with: urlRequest) {
        (data, response, error) in
        if let error = error { completion(nil, error); return }

        if let responseCode = response as? HTTPURLResponse {
            print("Response has status code: \(responseCode.statusCode)")
        }

        do {
            let retreived = try NetworkManager.shared.decoder.decode(ApiData.self, from: data!)
            completion(retreived, nil)
        } catch {
            print("Decoder error: ", error)
            completion(nil, error)
        }
        }.resume()
}

Upvotes: 3

Shehata Gamal
Shehata Gamal

Reputation: 100503

You can try

struct Root: Codable {
    let data: DataUnion
    let error: String?
}

enum DataUnion: Codable {
    case dataClass(DataClass)
    case datumArray([Datum])

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let x = try? container.decode([Datum].self) {
            self = .datumArray(x)
            return
        }
        if let x = try? container.decode(DataClass.self) {
            self = .dataClass(x)
            return
        }
        throw DecodingError.typeMismatch(DataUnion.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for DataUnion"))
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case .dataClass(let x):
            try container.encode(x)
        case .datumArray(let x):
            try container.encode(x)
        }
    }
}

struct Datum: Codable {
    let id: Int
}

struct DataClass: Codable {
    let id: Int
    let token: String
}

let res = try? JSONDecoder().decode(Root.self, from:data)

Upvotes: 0

Edvinas
Edvinas

Reputation: 147

If you have only two possible outcomes for your data, an option would be to try and parse data to one of the expected types, if that fails you know that the data is of other type and you can then handle it accordingly.

See this

Upvotes: 1

Related Questions