I would like to do a Post request to a url however i just learnt of new concept in Swift called Object mapping. All the tutorial i have learnt how to map the Json object to swift structs or classes but non shows me how to use these objects once they are mapped.
How do i access these objects such that i use them when doing a post request.
Here is example Json:
{
"country": "string",
"dateOfBirth": "string",
"email": "string",
"gender": "string",
"id": "string",
"interaction": {
"deviceOS": "string",
"deviceType": "string",
"interactionLocation": "string",
"interactionType": "string",
"timeStamp": "string"
},
"name": "string",
"occupation": "string",
"passportOrIDimage": "string",
"phoneNumber": "string",
"physicalAddress": "string",
"salutation": "string",
"surname": "string",
"userlogin": {
"accountNonExpired": true,
"accountNonLocked": true,
"credentialsNonExpired": true,
"enabled": true,
"password": "string",
"roles": [
{
"roleName": "string"
}
],
"username": "string"
}
}
Example Object mapping in swift 4 :
struct Register: Codable {
let country: String?
let dateOfBirth: String?
let email: String?
let gender: String?
let id: String?
let interaction: Interaction?
let name: String?
let occupation: String?
let passportOrIDimage: String?
let phoneNumber: String?
let physicalAddress: String?
let salutation: String?
let surname: String?
let userlogin: Userlogin?
enum CodingKeys: String, CodingKey {
case country = "country"
case dateOfBirth = "dateOfBirth"
case email = "email"
case gender = "gender"
case id = "id"
case interaction = "interaction"
case name = "name"
case occupation = "occupation"
case passportOrIDimage = "passportOrIDimage"
case phoneNumber = "phoneNumber"
case physicalAddress = "physicalAddress"
case salutation = "salutation"
case surname = "surname"
case userlogin = "userlogin"
}
}
struct Interaction: Codable {
let deviceOS: String?
let deviceType: String?
let interactionLocation: String?
let interactionType: String?
let timeStamp: String?
enum CodingKeys: String, CodingKey {
case deviceOS = "deviceOS"
case deviceType = "deviceType"
case interactionLocation = "interactionLocation"
case interactionType = "interactionType"
case timeStamp = "timeStamp"
}
}
struct Userlogin: Codable {
let accountNonExpired: Bool?
let accountNonLocked: Bool?
let credentialsNonExpired: Bool?
let enabled: Bool?
let password: String?
let roles: [Role]?
let username: String?
enum CodingKeys: String, CodingKey {
case accountNonExpired = "accountNonExpired"
case accountNonLocked = "accountNonLocked"
case credentialsNonExpired = "credentialsNonExpired"
case enabled = "enabled"
case password = "password"
case roles = "roles"
case username = "username"
}
}
struct Role: Codable {
let roleName: String?
enum CodingKeys: String, CodingKey {
case roleName = "roleName"
}
}
// MARK: Convenience initializers
extension Register {
init(data: Data) throws {
self = try JSONDecoder().decode(Register.self, from: data)
}
init(_ json: String, using encoding: String.Encoding = .utf8) throws {
guard let data = json.data(using: encoding) else {
throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
}
try self.init(data: data)
}
init(fromURL url: URL) throws {
try self.init(data: try Data(contentsOf: url))
}
func jsonData() throws -> Data {
return try JSONEncoder().encode(self)
}
func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
return String(data: try self.jsonData(), encoding: encoding)
}
}
extension Interaction {
init(data: Data) throws {
self = try JSONDecoder().decode(Interaction.self, from: data)
}
init(_ json: String, using encoding: String.Encoding = .utf8) throws {
guard let data = json.data(using: encoding) else {
throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
}
try self.init(data: data)
}
init(fromURL url: URL) throws {
try self.init(data: try Data(contentsOf: url))
}
func jsonData() throws -> Data {
return try JSONEncoder().encode(self)
}
func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
return String(data: try self.jsonData(), encoding: encoding)
}
}
extension Userlogin {
init(data: Data) throws {
self = try JSONDecoder().decode(Userlogin.self, from: data)
}
init(_ json: String, using encoding: String.Encoding = .utf8) throws {
guard let data = json.data(using: encoding) else {
throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
}
try self.init(data: data)
}
init(fromURL url: URL) throws {
try self.init(data: try Data(contentsOf: url))
}
func jsonData() throws -> Data {
return try JSONEncoder().encode(self)
}
func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
return String(data: try self.jsonData(), encoding: encoding)
}
}
extension Role {
init(data: Data) throws {
self = try JSONDecoder().decode(Role.self, from: data)
}
init(_ json: String, using encoding: String.Encoding = .utf8) throws {
guard let data = json.data(using: encoding) else {
throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
}
try self.init(data: data)
}
init(fromURL url: URL) throws {
try self.init(data: try Data(contentsOf: url))
}
func jsonData() throws -> Data {
return try JSONEncoder().encode(self)
}
func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
return String(data: try self.jsonData(), encoding: encoding)
}
}
I would to put the data i want to post in the params dictionary this where i am not sure how to do so using object mapping.
My post request :
var request = URLRequest(url: URL(string: "http://testURL")!)
request.httpMethod = "POST"
request.httpBody = try? JSONSerialization.data(withJSONObject: params as Any, options: [])
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
let session = URLSession.shared
let task = session.dataTask(with: request, completionHandler: { data, response, error -> Void in
print(response!)
do {
let json = try JSONSerialization.jsonObject(with: data!) as! Dictionary<String, AnyObject>
let alert = UIAlertController(title: "Response", message: "message", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil))
print(json)
} catch {
print("error")
}
})
task.resume()
}
if you observe in your struct
func jsonData() throws -> Data {
return try JSONEncoder().encode(self)
}
In your post request you can use like this
request.httpBody = try? objectOfCodableStruct.jsonData()
Hope it is helpful
Related
I'm having an issue decoding an API response.
So we have a NetworkManager class which we use to decode APIs. I have a simple GET endpoint that I need to retrieve a list of airports from. Here is the endpoint:
static let airports = Endpoint(url: "/test/airports")
Endpoint is defined as follows:
public struct Endpoint : Equatable {
public init(url: String? = nil, pattern: String? = nil, methods: [Test.HTTPMethod] = [.get], type: Test.EncodingType = .json)
}
Then in our network manager we have:
public func call<R: Decodable>(_ endpoint: Endpoint,
with args: [String: String]? = nil,
using method: HTTPMethod = .get,
expecting response: R.Type?,
completion: APIResponse<R>) {
call(endpoint, with: args, parameters: Nothing(),
using: method, posting: Nothing(), expecting: response, completion: completion)
}
My Airport model is as follows:
struct Airport: Codable {
let id: String
let name: String
let iata3: String
let icao4: String
let countryCode: String
}
And then I'm calling the endpoint like:
private func getAirportsList() {
API.client.call(.airports, expecting: [Airport].self) { (result, airports) in
print(airports)
}
}
Now I'm using Charles to proxy and I am getting the response I expect:
[{
"id": "5f92b0269c983567fc4b9683",
"name": "Amsterdam Schiphol",
"iata3": "AMS",
"icao4": "EHAM",
"countryCode": "NL"
}, {
"id": "5f92b0269c983567fc4b9685",
"name": "Bahrain International",
"iata3": "BAH",
"icao4": "OBBI",
"countryCode": "BH"
}, {
"id": "5f92b0269c983567fc4b968b",
"name": "Bankstown",
"iata3": "BWU",
"icao4": "YSBK",
"countryCode": "AU"
}]
But in my getAirports() method, airports is nil. I'm really struggling to see why. Clearly the endpoint is being hit correctly but my decoding is failing.
Edit:
Full method:
private func call<P: Encodable, B: Encodable, R: Decodable>(_ endpoint: Endpoint,
with args: [String: String]? = nil,
parameters params: P?,
using method: HTTPMethod = .get,
posting body: B?,
expecting responseType: R.Type?,
completion: APIResponse<R>) {
// Prepare our URL components
guard var urlComponents = URLComponents(string: baseURL.absoluteString) else {
completion?(.failure(nil, NetworkError(reason: .invalidURL)), nil)
return
}
guard let endpointPath = endpoint.url(with: args) else {
completion?(.failure(nil, NetworkError(reason: .invalidURL)), nil)
return
}
urlComponents.path = urlComponents.path.appending(endpointPath)
// Apply our parameters
applyParameters: if let parameters = try? params.asDictionary() {
if parameters.count == 0 {
break applyParameters
}
var queryItems = [URLQueryItem]()
for (key, value) in parameters {
if let value = value as? String {
let queryItem = URLQueryItem(name: key, value: value)
queryItems.append(queryItem)
}
}
urlComponents.queryItems = queryItems
}
// Try to build the URL, bad request if we can't
guard let urlString = urlComponents.url?.absoluteString.removingPercentEncoding,
var url = URL(string: urlString) else {
completion?(.failure(nil, NetworkError(reason: .invalidURL)), nil)
return
}
if let uuid = UIDevice.current.identifierForVendor?.uuidString, endpoint.pattern == "/logging/v1/device/<device_id>" {
let us = "http://192.168.6.128:3000/logging/v1/device/\(uuid)"
guard let u = URL(string: us) else { return }
url = u
}
// Can we call this method on this endpoint? If not, lets not try to continue
guard endpoint.httpMethods.contains(method) else {
completion?(.failure(nil, NetworkError(reason: .methodNotAllowed)), nil)
return
}
// Apply debug cookie
if let debugCookie = debugCookie {
HTTPCookieStorage.shared.setCookies(
HTTPCookie.cookies(
withResponseHeaderFields: ["Set-Cookie": debugCookie],
for:url
), for: url, mainDocumentURL: url)
}
// Build our request
var request = URLRequest(url: url)
request.httpMethod = method.rawValue
if let headers = headers {
for (key, value) in headers {
request.setValue(value, forHTTPHeaderField: key)
}
}
// If we are posting, safely retrieve the body and try to assign it to our request
if !(body is NothingProtocol) {
guard let body = body else {
completion?(.failure(nil, NetworkError(reason: .buildingPayload)), nil)
return
}
do {
let result = try encode(body: body, type: endpoint.encodingType)
request.httpBody = result.data
request.setValue(result.headerValue, forHTTPHeaderField: "Content-Type")
} catch {
completion?(.failure(nil, NetworkError(reason: .buildingPayload)), nil)
return
}
}
// Build our response handler
let task = session.dataTask(with: request as URLRequest) { (rawData, response, error) in
// Print some logs to help track requests
var debugOutput = "URL\n\(url)\n\n"
if !(params is Nothing.Type) {
debugOutput.append(contentsOf: "PARAMETERS\n\(params.asJSONString() ?? "No Parameters")\n\n")
}
if !(body is Nothing.Type) {
debugOutput.append(contentsOf: "BODY\n\(body.asJSONString() ?? "No Body")\n\n")
}
if let responseData = rawData {
debugOutput.append(contentsOf: "RESPONSE\n\(String(data: responseData, encoding: .utf8) ?? "No Response Content")")
}
Logging.client.record(debugOutput, domain: .network, level: .debug)
guard let httpResponse = response as? HTTPURLResponse else {
guard error == nil else {
completion?(.failure(nil, NetworkError(reason: .unwrappingResponse)), nil)
return
}
completion?(.failure(nil, NetworkError(reason: .invalidResponseType)), nil)
return
}
let statusCode = httpResponse.statusCode
// We have an error, return it
guard error == nil, NetworkManager.successStatusRange.contains(statusCode) else {
var output: Any?
if let data = rawData {
output = (try? JSONSerialization.jsonObject(with: data,
options: .allowFragments)) ?? "Unable to connect"
Logging.client.record("Response: \(String(data: data, encoding: .utf8) ?? "No error data")", domain: .network)
}
completion?(.failure(statusCode, NetworkError(reason: .requestFailed, json: output)), nil)
return
}
// Safely cast the responseType we are expecting
guard let responseType = responseType else {
completion?(.failure(statusCode, NetworkError(reason: .castingToExpectedType)), nil)
return
}
// If we are expecting nothing, return now (since we will have nothing!)
if responseType is Nothing.Type {
completion?(.success(statusCode), nil)
return
}
guard let data = rawData else {
assertionFailure("Could not cast data from payload when we passed pre-cast checks")
return
}
// Decode the JSON and cast to our expected response type
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let responseObject = try decoder.decode(responseType, from: data)
completion?(.success(statusCode), responseObject)
return
} catch let error {
let content = try? JSONSerialization.jsonObject(with: data, options: .allowFragments)
Logging.client.record("Failed to build codable from JSON: \(String(describing: content))\n\nError: \(error)", domain: .network, level: .error)
assertionFailure("Failed to build codable from JSON: \(error)")
completion?(.failure(statusCode, NetworkError(reason: .castingToExpectedType)), nil)
return
}
}
// Submit our request
task.resume()
}
http://173.249.20.137:9000/apiapp/coupon GET method .
when I request via URLSession and Postman I get two different results. Actually postman data is correct, but the URL session has always the same response whether I add or delete data it is not going to update. Can anybody please give a look. if it is happening with me only or something wrong at the backend server.
I have tested requesting data with URLSession.shared and postman.
I actually like to have the data I get via postman through URLSession request too.
func getAvailableCoupons(urlString:String, completion: #escaping (_
product: Any, _ error: Error?) -> Void){
guard let url = URL(string: urlString) else {return}
let task = URLSession.shared.dataTask(with: url) { (data, response,
error) in
let jsonDecoder = JSONDecoder()
guard let dataResponse = data,
error == nil else {
print(error?.localizedDescription ?? "Response Error")
return }
let statusCode = (response as! HTTPURLResponse).statusCode
let responseJSON = try? JSONSerialization.jsonObject(with: dataResponse, options: [])
if let responseJSON = responseJSON as? [String: Any] {
if statusCode == 200 {
do {
let jsonData = try JSONSerialization.data(withJSONObject: responseJSON, options: [])
let responseData = try jsonDecoder.decode(CoupensResponseModel.self, from:jsonData)
completion(responseData, nil)
} catch let error {
print("error when parshing json response \(error)")
completion(error, nil )
}
} else if statusCode == 404{
completion(" 404 not found", nil )
} else {
print("fatal error \(error?.localizedDescription ?? "big errror")")
}
}
}
task.resume()
}
import Foundation
// MARK: - CoupensResponseModel
struct CoupensResponseModel: Codable {
let couponDetails: [CouponDetail]?
enum CodingKeys: String, CodingKey {
case couponDetails = "coupon_details"
}
}
// MARK: - CouponDetail
struct CouponDetail: Codable {
let id: Int?
let vouchersusageSet: [VouchersusageSet]?
let couponCode: String?
let minimumSpend: Int?
let expiryDate, createdDate: String?
let discountPrice, discountPercent: Int?
let discountBasis: DiscountBasis?
let couponImage: String?
let couponType: String?
enum CodingKeys: String, CodingKey {
case id
case vouchersusageSet = "vouchersusage_set"
case couponCode = "coupon_code"
case minimumSpend = "minimum_spend"
case expiryDate = "expiry_date"
case createdDate = "created_date"
case discountPrice = "discount_price"
case discountPercent = "discount_percent"
case discountBasis = "discount_basis"
case couponImage = "coupon_image"
case couponType = "coupon_type"
}
}
enum DiscountBasis: String, Codable {
case amount = "amount"
case percent = "percent"
}
// MARK: - VouchersusageSet
struct VouchersusageSet: Codable {
let id: Int?
let itemObj: ItemObj?
let voucherObj: Int?
let sectionObj, categoryObj: Int?
}
// MARK: - ItemObj
struct ItemObj: Codable {
let id: Int?
let code, productName: String?
let productPrice, discountedPrice: Int?
let productDescription, itemImage: String?
let categoryObj: CouponCategoryObj?
enum CodingKeys: String, CodingKey {
case id, code
case productName = "product_name"
case productPrice = "product_price"
case discountedPrice = "discounted_price"
case productDescription = "product_description"
case itemImage = "item_image"
case categoryObj
}
}
// MARK: - CouponCategoryObj
struct CouponCategoryObj: Codable {
let id: Int?
let categoryCode, categoryName: String?
let categoryImage: CouponJSONNull?
let sectionObj: Int?
enum CodingKeys: String, CodingKey {
case id
case categoryCode = "category_code"
case categoryName = "category_name"
case categoryImage = "category_image"
case sectionObj
}
}
// MARK: - Encode/decode helpers
class CouponJSONNull: Codable, Hashable {
public static func == (lhs: CouponJSONNull, rhs: CouponJSONNull) ->
Bool {
return true
}
public var hashValue: Int {
return 0
}
public func hash(into hasher: inout Hasher) {
// No-op
}
public init() {}
public required init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if !container.decodeNil() {
throw DecodingError.typeMismatch(CouponJSONNull.self,
DecodingError.Context(codingPath: decoder.codingPath,
debugDescription:
"Wrong type for CouponJSONNull"))
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encodeNil()
}
}
Try the following method
let headers = [
"Cache-Control": "no-cache",
]
let request = NSMutableURLRequest(url: NSURL(string: "http://173.249.20.137:9000/apiapp/coupon")! as URL,
cachePolicy: .useProtocolCachePolicy,
timeoutInterval: 10.0)
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers
let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
if (error != nil) {
print(error)
} else {
let string = String(data: data!, encoding: .utf8) ?? ""
print(string)
}
})
dataTask.resume()
Please look on response both are same.
Postman response :
https://jsoneditoronline.org/?id=7b94ef69a3344164aa4a96423fdbf9db
Code response :
https://jsoneditoronline.org/?id=6e5a7d221d9c4c818f1d46fc893031fe
How can I write a generic wrapper for alamofire request ?
How can I convert the POST and GET function to Generic function in the following code?
I need to have generic request functions that show different behaviors depending on the type of data received.
Also, Can the response be generic?
My non-generic code is fully outlined below
import Foundation
import Alamofire
import SwiftyJSON
// for passing string body
extension String: ParameterEncoding {
public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
var request = try urlRequest.asURLRequest()
request.httpBody = data(using: .utf8, allowLossyConversion: false)
return request
}
}
public class ConnectionManager {
func Post (FirstName: String, LastName: String, NationalID: String, NationalCode: String, BirthDate: Date,Image: String,isFemale: Bool,Age:Int64,Avg:Double, completion: #escaping CompletionHandler) {
let body: [String:Any] = [
"FirstName":FirstName,
"LastName": LastName,
"NationalID": NationalID,
"NationalCode": NationalCode,
"BirthDate":BirthDate,
"Image": Image,
"isFemale": isFemale,
"Age": Age,
"Avg": Avg
]
Alamofire.request(BASE_URL, method: .post, parameters: body, encoding: JSONEncoding.default, headers: HEADER).responseJSON { (response) in
if response.response?.statusCode == 200 {
guard let data = response.result.value else { return }
print(data)
completion(true)
} else {
print("error reg auth service")
guard let er = response.result.value else { return }
print(er)
completion(false)
debugPrint(response.result.error as Any)
}
}
}
func get(FirstName: String, LastName: String, NationalID: String, NationalCode: String, BirthDate: Date,Image: String,isFemale: Bool,Age:Int64,Avg:Double, completion: #escaping CompletionHandler) -> [Person] {
let body: [String:Any] = [
"FirstName":FirstName,
"LastName": LastName,
"NationalID": NationalID,
"NationalCode": NationalCode,
"BirthDate":BirthDate,
"Image": Image,
"isFemale": isFemale,
"Age": Age,
"Avg": Avg
]
Alamofire.request(BASE_URL, method: .get, parameters: body, encoding: JSONEncoding.default, headers: HEADER).responseJSON { (response) in
if response.response?.statusCode == 200 {
print("no error login in authservice")
guard let data = response.result.value else { return }
print(data)
completion(true)
}
else if response.response?.statusCode == 400 {
print("error 400 login in authservice")
guard let er = response.result.value else { return }
print(er)
debugPrint(response.result.error as Any)
completion(false)
} else {
print("error ## login in authservice")
guard let er = response.result.value else { return }
print(er)
debugPrint(response.result.error as Any)
completion(false)
}
}
return [Person]()
}
}
The best idea is to use the URLRequestConvertible Alamofires protocol and create your own protocol and simple structs for every API request:
import Foundation
import Alamofire
protocol APIRouteable: URLRequestConvertible {
var baseURL: String { get }
var path: String { get }
var method: HTTPMethod { get }
var parameters: Parameters? { get }
var encoding: ParameterEncoding { get }
}
extension APIRouteable {
var baseURL: String { return URLs.baseURL }
// MARK: - URLRequestConvertible
func asURLRequest() throws -> URLRequest {
let url = try baseURL.asURL().appendingPathComponent(path)
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = method.rawValue
return try encoding.encode(urlRequest, with: parameters)
}
}
and request can look like this:
struct GetBooks: APIRouteable {
var path: String
var method: HTTPMethod
var parameters: Parameters?
var encoding: ParameterEncoding
}
and inside the APIClient prepare the generic method:
func perform<T: Decodable>(_ apiRoute: APIRouteable,
completion: #escaping (Result<T>) -> Void) {
let dataRequest = session
.request(apiRoute)
dataRequest
.validate(statusCode: 200..<300)
.responseDecodable(completionHandler: { [weak dataRequest] (response: DataResponse<T>) in
dataRequest.map { debugPrint($0) }
let responseData = response.data ?? Data()
let string = String(data: responseData, encoding: .utf8)
print("Repsonse string: \(string ?? "")")
switch response.result {
case .success(let response):
completion(.success(response))
case .failure(let error):
completion(.failure(error))
}
})
}
and use it:
func getBooks(completion: #escaping (Result<[Book]>) -> Void) {
let getBooksRoute = APIRoute.GetBooks()
perform(getBooksRoute, completion: completion)
}
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()
}
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()
}
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
}
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
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)
I am trying to post 2 parameters (email: and password) to get a response from the server with detailed user information, I build API to handle this and get a good response using Postman, but when I tried to implement this with Swift4 new urlsession JSON decode and encode, it keeps failing and I got error on decoding the response data.
this my JSON response when using Postman:
{
"error": "false",
"message": "downloaded",
"UserInfo": {
"id": 5,
"email": "abc#hotmail.com",
"lastname": "Bence",
"name": "Mark",
"phone": "1234567",
"add1": "333",
"add2": "444",
"city": "ott",
"postalcode": "tttttt"
}
}
My struct file:
import UIKit
struct loginPost: Encodable {
let email: String
let password: String
}
struct User: Decodable {
let error: String?
let message: String?
let UserInfo: [UserData]
}
struct UserData: Codable {
let id: Int?
let email: String?
let lastname: String?
let name: String?
let phone: String?
let add1: String?
let add2: String?
let city: String
let postalcode: String?
}
My Function
func downloadJson() {
let url = URL(string: http://192.168.0.10/api/login_hashed.php)
guard let downloadURL = url else { return }
//POST Req
var request = URLRequest(url: downloadURL)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")
let newpost = loginPost(email: "abc#hotmail.com", password: "123456")
do {
let jsonBody = try JSONEncoder().encode(newpost)
request.httpBody = jsonBody
print(jsonBody)
}catch{
print("some error")
}
URLSession.shared.dataTask(with: request) { data, urlResponse, error in
guard let data = data, error == nil, urlResponse != nil else {
print("something is wrong with url")
return
}
print("downloaded..")
do
{
let decoder = JSONDecoder()
let downloaduser = try decoder.decode(User.self, from: data)
self.logmessage = downloaduser.message!
print(self.logmessage)
DispatchQueue.main.async {
// self.tableView.reloadData()
}
} catch {
print("something wrong with decode")
}
}.resume()
}
I have figured it out finally, thanks,.
I just want to mention the cause of this error and share my experience.
The main cause is the way you send JSON and receive the incoming response. you should know exactly how the data look in order to create your struct the acceptable way.
My return data is just simple 2 line of text and array of text, my struct was:
import UIKit
struct loginPost: Encodable {
let email: String
let password: String
}
struct User: Decodable {
let error: String?
let message: String?
let UserInfo: [UserData]
}
struct UserData: Codable {
let id: Int?
let email: String?
let lastname: String?
let name: String?
let phone: String?
let add1: String?
let add2: String?
let city: String
let postalcode: String?
}
my mistake on line 18
let UserInfo: [UserData]
it should be
let UserInfo: UserData?
without the square bracket.
one more important point, always try to catch the decode error and it's dicription by implementing }catch let JsonErr {, it will give you exactly why the decode not working.
in my case:
downloaded.. something wrong after downloaded
typeMismatch(Swift.Array, Swift.DecodingError.Context(codingPath:
[h2h.User.(CodingKeys in _E33F61CC43E102744E4EF1B7E9D7EDDE).UserInfo],
debugDescription: "Expected to decode Array but found a
dictionary instead.", underlyingError: nil))
And make sure to make your server API to accept JSON format application/json and
decode what you send in order to receive what you looking for;
php code service API
$UserData = json_decode(file_get_contents("php://input"), true);
Simplest and easy way to decode the json.
MUST TRY
struct Welcome: Codable {
let error, message: String?
let userInfo: UserInfo?
enum CodingKeys: String, CodingKey {
case error, message
case userInfo = "UserInfo"
}
}
// MARK: - UserInfo
struct UserInfo: Codable {
let id: Int?
let email, lastname, name, phone: String?
let add1, add2, city, postalcode: String?
}
After that in your code , when you get response from api then write
let decoder = JSONDecoder()
let obj = try! decoder.decode(Welcome.self, from: jsonData!)
Print(obj)
List item
This Will work Are You Creating Model Is Wrong
struct loginPost: Encodable {
let email: String
let password: String
}
struct Users:Codable {
var error:String?
var message:String?
var UserInfo:UserDetails?
}
struct UserDetails:Codable {
let id: Int?
let email: String?
let lastname: String?
let name: String?
let phone: String?
let add1: String?
let add2: String?
let city: String
let postalcode: String?
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
downloadJson()
}
func downloadJson() {
let url = URL(string: "http://192.168.0.10/api/login_hashed.php")
guard let downloadURL = url else { return }
//POST Req
var request = URLRequest(url: downloadURL)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")
let newpost = loginPost(email: "abc#hotmail.com", password: "123456")
do {
let jsonBody = try JSONEncoder().encode(newpost)
request.httpBody = jsonBody
print(jsonBody)
}catch{
print("some error")
}
URLSession.shared.dataTask(with: request) { data, urlResponse, error in
guard let data = data, error == nil, urlResponse != nil else {
print("something is wrong with url")
return
}
print("downloaded..")
do
{
let decoder = JSONDecoder()
let downloaduser = try decoder.decode(Users.self, from: data)
// self.logmessage = downloaduser.message!
// print(self.logmessage)
DispatchQueue.main.async {
// self.tableView.reloadData()
}
} catch {
print("something wrong with decode")
}
}.resume()
}
}