Decoding dynamic JSON structure in swift 4

孤街浪徒 提交于 2019-12-02 12:03:18

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

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

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
}

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