Decoding dynamic JSON structure in swift 4

十年热恋 提交于 2019-12-02 15:42:54

问题


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()
}

回答1:


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()
}



回答2:


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




回答3:


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
}



回答4:


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)


来源:https://stackoverflow.com/questions/54019877/decoding-dynamic-json-structure-in-swift-4

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