I have a decodable class:
struct AuthenticationResponse : Decodable {
var status: String
var error: Error
var access_token: String? = ""
var expires_in: Double? = 0
var token_type: String? = ""
var scope: String? = ""
var refresh_token: String? = "
}
struct Error : Decodable {
var desc: String
var code: String
}
In the Error class I have:
And to decode to this class, I have:
URLSession.shared.dataTask(with: request) { (data:Data?, response:URLResponse?, error:Error?) in
if let jsonData = data{
let decoder = JSONDecoder()
print("hey")
print("response: \(String(data:jsonData, encoding:.utf8))")
completion(try! decoder.decode(AuthenticationResponse.self, from: jsonData))
}
}.resume()
As some of the responses I receive are (Success response):
{
“status”: “SUCCESS” “error”: null, "access_token":
"MWVmOWQxMDYwMjQyNDQ4NzQyNTdkZjQ3NmI4YmVjMGZjZGM5N2IyZmNkOTA1 N2M0NDUzODEwYjM5ZWQyNGNkZg",
"expires_in": 3600, "token_type": "bearer", "scope": null,
"refresh_token":
"ZGEwOGZiOWZhMzhhYjBmMzAyOGRmZTA5NjJhMjY2MTk3YzMyMmE1ZDlkNWI2N mJjYmIxMjNkMjE1NWFhNWY0Mg"
}
And then a failed response just contains an error object with desc and code in it.
What i am trying to achieve is a decodable class suitable for both scenarios (When a response is successful and failed) however im not sure how to achieve this. I'm aware i can make 2 separate decodable classes but this would make things messier as i'd have to determine if the response is an error and populate to return different classes.
Does anyone know how i should acheive this>
I will give it a try, but first we need to sort out what I consider a somewhat shoddy question. Since Error is the name of a (famous and widely used) protocol it should be renamed and since you want to be able to leave it empty in your AuthenticationResponse it must obviously be an optional there (bearing the question why it is in the Response at all, but I will leave this aside). This leaves us with the following:
struct AuthError : Decodable {
var desc: String
var code: String
}
struct AuthenticationResponse : Decodable {
var status: String
var error: AuthError?
var access_token: String? = ""
var expires_in: Double? = 0
var token_type: String? = ""
var scope: String? = ""
var refresh_token: String? = ""
}
Then we need some example data for the two relevant cases in question, I used:
let okData = """
{
"status": "SUCCESS",
"error": null,
"access_token":
"MWVmOWQxMDYwMjQyNDQ4NzQyNTdkZjQ3NmI4YmVjMGZjZGM5N2IyZmNkOTA1N2M0NDUzODEwYjM5ZWQyNGNkZg",
"expires_in": 3600,
"token_type": "bearer",
"scope": null,
"refresh_token":
"ZGEwOGZiOWZhMzhhYjBmMzAyOGRmZTA5NjJhMjY2MTk3YzMyMmE1ZDlkNWI2NmJjYmIxMjNkMjE1NWFhNWY0Mg"
}
""".data(using: .utf8)!
let errData = """
{
"desc": "username or password incorrect",
"code": "404"
}
""".data(using: .utf8)!
Now we can define a single enum return type which allows for all our cases:
enum AuthResult {
case ok(response: AuthenticationResponse)
case authError(error: AuthError)
case parseError(description: String)
case fatal
}
which finally allows us to write our parse function for the received authentication data:
func parse(_ jsonData:Data) -> AuthResult {
let decoder = JSONDecoder()
do {
let authRes = try decoder.decode(AuthenticationResponse.self, from: jsonData)
return .ok(response: authRes)
} catch {
do {
let errRes = try decoder.decode(AuthError.self, from: jsonData)
return .authError(error: errRes)
} catch let errDecode {
return .parseError(description: errDecode.localizedDescription)
}
}
}
All this in a Playground will permit usage as in
switch parse(okData) {
case let .ok(response):
print(response)
case let .authError(error):
print(error)
case let .parseError(description):
print("You threw some garbage at me and I was only able to \(description)")
default:
print("don't know what to do here")
}
That is still elegant compared to the mess you would make in most other languages, but the call is still out on wether it would not make more sense to just define AuthenticationResponse as the (regular) return type of the parse function and provide the rest by throwing some enum (conforming to Error) and some suitable payload.
Coming (mainly) from Java I still shun from using exceptions as "somewhat" regular control flow (as in a "regular" login failure), but given Swifts much more reasonable approach to exceptions this might have to be reconsidered.
Anyways, this leaves you with a function to parse either case of your services replies and a decent way to handle them in a "uniform" manner. As you might not be able to modify the behaviour of the service handling your request this might be the only viable option. However, if you are able to modify the service you should strive for a "uniform" reply that would be parseable by a single call to JSONDecoder.decode. You would still have to interpret the optionals (as you should in the above example, since they are still a pain to work with, even given Swifts brilliant compiler support forcing you to "do the right thing"), but it would make your parsing less error prone.
Related
I know this question is asked a lot, but I can't figure out how to apply any answers to my program. Sorry in advance this async stuff makes absolutely zero sense to me.
Basically, I have a button in SwiftUI that, when pressed, calls a function that makes two API calls to Google Sheets using Alamofire and GoogleSignIn.
Button("Search") {
if fullName != "" {
print(SheetsAPI.nameSearch(name: fullName, user: vm.getUser()) ?? "Error")
}
}
This function should return the values of some cells on success or nil on an error. However, it only ever prints out "Error". Here is the function code.
static func nameSearch<S: StringProtocol>(name: S, advisory: S = "", user: GIDGoogleUser?) -> [String]? {
let name = String(name)
let advisory = String(advisory)
let writeRange = "'App Control'!A2:C2"
let readRange = "'App Control'!A4:V4"
// This function can only ever run when user is logged in, ! should be fine?
let user = user!
let parameters: [String: Any] = [
"range": writeRange,
"values": [
[
name,
nil,
advisory
]
]
]
// What I want to be returned
var data: [String]?
// Google Identity said use this wrapper so that the OAuth tokens refresh
user.authentication.do { authentication, error in
guard error == nil else { return }
guard let authentication = authentication else { return }
// Get the access token to attach it to a REST or gRPC request.
let token = authentication.accessToken
let headers: HTTPHeaders = ["Authorization": "Bearer \(token)"]
AF.request("url", method: .put, parameters: parameters, encoding: JSONEncoding.default, headers: headers).responseString { response in
switch response.result {
case .success:
// I assume there is a better way to make two API calls...
AF.request("anotherURL", headers: headers).responseDecodable(of: NameResponseModel.self) { response2 in
switch response2.result {
case .success:
guard let responseData = response2.value else { return }
data = responseData.values[0]
// print(responseData.values[0]) works fine
case .failure:
print(response2.error ?? "Unknown error.")
data = nil
}
}
case .failure:
print(response.error ?? "Unknown error.")
data = nil
}
}
}
// Always returns nil, "Unknown error." never printed
return data
}
The model struct for my second AF request:
struct NameResponseModel: Decodable { let values: [[String]] }
An example API response for the second AF request:
{
"range": "'App Control'!A4:V4",
"majorDimension": "ROWS",
"values": [
[
"Bob Jones",
"A1234",
"Cathy Jones",
"1234 N. Street St. City, State 12345"
]
]
}
I saw stuff about your own callback function as a function parameter (or something along those lines) to handle this, but I was completely lost. I also looked at Swift async/await, but I don't know how that works with callback functions. Xcode had the option to refactor user.authentication.do { authentication, error in to let authentication = try await user.authentication.do(), but it threw a missing parameter error (the closure it previously had).
EDIT: user.authentication.do also returns void--another reason the refactor didn't work (I think).
There is probably a much more elegant way to do all of this so excuse the possibly atrocious way I did it.
Here is the link to Google Identity Wrapper info.
Thanks in advance for your help.
Solved my own problem.
It appears (according to Apple's async/await intro video) that when you have an unsupported callback that you need to run asynchronously, you wrap it in something called a Continuation, which allows you to manually resume the function on the thread, whether throwing or returning.
So using that code allows you to run the Google Identity token refresh with async/await.
private static func auth(_ user: GIDGoogleUser) async throws -> GIDAuthentication? {
typealias AuthContinuation = CheckedContinuation<GIDAuthentication?, Error>
return try await withCheckedThrowingContinuation { (continuation: AuthContinuation) in
user.authentication.do { authentication, error in
if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: authentication)
}
}
}
}
static func search(user: GIDGoogleUser) async throws {
// some code
guard let authentication = try await auth(user) else { ... }
// some code
}
I then ran that before using Alamofire's built-in async/await functionality for each request (here's one).
let dataTask = AF.request(...).serializingDecodable(NameResponseModel.self)
let response = try await dataTask.value
return response.values[0]
EDIT:
I am trying my level best to make my question simpler,
here what I am trying to get a solution for is, I have an API and if my data is valid the API will give the correct response, for which I need to decode with the respective struct in swift.
also if my data is wrong the API will fail and it will produce an error response which is a different struct.
with the use of combine, I only could decode a single struct type.
SO how do I make my decode accept any type?
Generics is one way I hoped to solve but here the protocol that I need to implement is an issue I believe restricting me from using generics.
thanks for giving it a try.
// MARK: - ResponseStruct Model
struct ResponseStruct: Codable {
}
//MARK: -Protocol
public protocol RequestProtocol{
associatedtype ResponseOutput
func fetchFunction() -> AnyPublisher<ResponseOutput, Error>
}
//Mark: - Implementation
struct RequestStruct: Codable, RequestProtocol {
typealias ResponseOutput = ResponseStruct
func fetchFunction() -> AnyPublisher<ResponseOutput, Error> {
let networkManager = NetworkManager()
do {
return try networkManager.apiCall(url: url, method: .post, body: JSONEncoder().encode(self))
.decode(type: ResponseStruct.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
} catch {
}
}
}
Above this is the code, and this is fine if the API call works but if the call fails I will get an error response, so how to decode that struct in a combined way? I don't want to write another call for that and I am hoping to get something to do with Failure in the combine. or CAN I MAKE THE associated type (see protocol) generic?
I beg for your patience. I think I understand the problem, but I'm having a hard time lining it up to the code you've given. Your fetchFunction is particularly odd and I don't understand what your protocol is trying to accomplish.
Let me start with the problem statement and explore a solution. I'll do it step-by-step so this will be a long response. The tl;dr is a Playground at the end.
I have an API and if my data is valid the API will give the correct response, for which I need to decode with the respective struct in swift.
If my data is wrong the API will fail and it will produce an error response which is a different struct.
So we need two structs. One for success and one for failure. I'll make some up:
struct SuccessfulResult : Decodable {
let interestingText : String
}
struct FailedResult : Decodable {
let errorCode : Int
let failureReason : String
}
Based on that, request to the network can:
Return success data to decode into SuccessfulResult
Return failure data to decode into FailedResult
Fail because of a low-level error (e.g. The network is unreachable).
Let's create a type for "The network worked just fine and gave me either success data or failure data":
enum NetworkData {
case success(Data)
case failure(Data)
}
I'll use Error for low-level errors.
With those types an API request can be represented as a publisher of the type AnyPublisher<NetworkData, Error>
But that's not what you asked for. You want to parse the data into SuccessfulResult or FailedResult. This also raises the possibility that JSON parsing fails which I will also sweep under the rug of a generic Error.
We need a data type to represent the parsed variant of NetworkData:
enum ParsedNetworkData {
case success(SuccessfulResult)
case failure(FailedResult)
}
Which means the real Network request type you've asked for is a publisher of the type AnyPublisher<ParsedNetworkData,Error>
We can write a function to transform a Data bearing network request, AnyPublisher<NetworkData,Error>, into an AnyPublisher<ParsedNetworkData,Error>.
One way to write that function is:
func transformRawNetworkRequest(_ networkRequest: AnyPublisher<NetworkData,Error>) -> AnyPublisher<ParsedNetworkData, Error> {
let decoder = JSONDecoder()
return networkRequest
.tryMap { networkData -> ParsedNetworkData in
switch(networkData) {
case .success(let successData):
return ParsedNetworkData.success(try decoder.decode(SuccessfulResult.self, from: successData))
case .failure(let failureData):
return ParsedNetworkData.failure(try decoder.decode(FailedResult.self, from: failureData))
}
}
.eraseToAnyPublisher()
}
To exercise the code we can write a function to create a fake network request and add some code that tries things out. Putting it all together into a playground you get:
import Foundation
import Combine
struct SuccessfulResult : Decodable {
let interestingText : String
}
struct FailedResult : Decodable {
let errorCode : Int
let failureReason : String
}
enum NetworkData {
case success(Data)
case failure(Data)
}
enum ParsedNetworkData {
case success(SuccessfulResult)
case failure(FailedResult)
}
func transformRawNetworkRequest(_ networkRequest: AnyPublisher<NetworkData,Error>) -> AnyPublisher<ParsedNetworkData, Error> {
let decoder = JSONDecoder()
return networkRequest
.tryMap { networkData -> ParsedNetworkData in
switch(networkData) {
case .success(let successData):
return ParsedNetworkData.success(try decoder.decode(SuccessfulResult.self, from: successData))
case .failure(let failureData):
return ParsedNetworkData.failure(try decoder.decode(FailedResult.self, from: failureData))
}
}
.eraseToAnyPublisher()
}
func fakeNetworkRequest(shouldSucceed: Bool) -> AnyPublisher<NetworkData,Error> {
let successfulBody = """
{ "interestingText" : "This is interesting!" }
""".data(using: .utf8)!
let failedBody = """
{
"errorCode" : -4242,
"failureReason" : "Bogus! Stuff went wrong."
}
""".data(using: .utf8)!
return Future<NetworkData,Error> { fulfill in
let delay = Set(stride(from: 100, to: 600, by: 100)).randomElement()!
DispatchQueue.global(qos: .background).asyncAfter(
deadline: .now() + .milliseconds(delay)) {
if(shouldSucceed) {
fulfill(.success(NetworkData.success(successfulBody)))
} else {
fulfill(.success(NetworkData.failure(failedBody)))
}
}
}.eraseToAnyPublisher()
}
var subscriptions = Set<AnyCancellable>()
let successfulRequest = transformRawNetworkRequest(fakeNetworkRequest(shouldSucceed: true))
successfulRequest
.sink(receiveCompletion:{ debugPrint($0) },
receiveValue:{ debugPrint("Success Result \($0)") })
.store(in: &subscriptions)
let failedRequest = transformRawNetworkRequest(fakeNetworkRequest(shouldSucceed: false))
failedRequest
.sink(receiveCompletion:{ debugPrint($0) },
receiveValue:{ debugPrint("Failure Result \($0)") })
.store(in: &subscriptions)
I am trying to write a simple function handling authentication POST requests that return JWT tokens.
My LoopBack 4 API returns the token as a JSON packet in the following format:
{ "token": "my.jwt.token" }
In case of an error, the following gets returned instead:
{
"error": {
"statusCode": 401,
"name": "UnauthorizedError",
"message": "Invalid email or password."
}
}
As you can see, these types are completely different, they don't have any common properties.
I've defined the following Swift structures to represent them:
// Success
struct Token: Decodable {
let token: String
}
// Error
struct TokenError: Decodable {
let error: ApiError
}
struct ApiError: Decodable {
let statusCode: Int
let name: String
let message: String
}
The signature of the authentication request that returns Swift Generics:
#available(iOS 15.0.0, *)
func requestToken<T: Decodable>(_ user: String, _ password: String) async throws -> T
I've been trying to unit test this function but Swift requires me to declare the type of the result up front:
let result: Token = try await requestToken(login, password)
This works perfectly fine for the happy path but if the authentication is unsuccessful, a The data couldn’t be read because it is missing. error gets thrown. I can certainly do-catch it but I haven't been able to cast the result to my TokenError type in order to access its properties.
I have come across a few threads on StackOverflow where the general advice is to represent the success and error types by a common protocol but I've had no luck with that either due to a conflict with the Decodable protocol that the response types already conform to.
So the question is whether it is possible to work with both success and error result variables returned by my requestToken function.
The most natural way, IMO is to throw ApiErrors, so that they can be handled in the same way as other errors. That would look like this:
Mark ApiError as an Error type:
extension ApiError: Error {}
Now you can decode Token directly, and it will throw ApiError if there's an API error, or DecodingError if the data is corrupted. (Note the use of try? in the first decode and try in the else decode. This way it throws if the data can't be decoded at all.)
extension Token: Decodable {
enum CodingKeys: CodingKey {
case token
}
init(from decoder: Decoder) throws {
if let container = try? decoder.container(keyedBy: CodingKeys.self),
let token = try? container.decode(String.self, forKey: .token)
{
self.init(token: token)
} else {
throw try TokenError(from: decoder).error
}
}
}
// Usage if you want to handle ApiErrors specially
do {
try JSONDecoder().decode(Token.self, from: data)
} catch let error as ApiError {
// Handle ApiErrors
} catch let error {
// Handle other errors
}
Another approach is to keep ApiErrors separate from other errors, in which case there are three possible ways requestToken can return. It can return a Token, or it can return a TokenError, or it can throw a parsing error. Throwing an error is handled by throws. Token/TokenError require an "or" type, which is an enum. This could be done with a Result, but that might be a little confusing, since the routine also throws. Instead I'll be explicit.
enum TokenRequestResult {
case token(Token)
case error(ApiError)
}
Now you can make this Decodable by first trying to decode it as a Token, and if that fails, try decoding it as a TokenError and extracting the ApiError from that:
extension TokenRequestResult: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let token = try?
container.decode(Token.self) {
self = .token(token)
} else {
self = .error(try container.decode(TokenError.self).error)
}
}
}
To use this, you just need to switch:
let result = try JSONDecoder().decode(TokenRequestResult.self, from: token)
switch result {
case .token(let token): // use token
case .error(let error): // use error
}
I have a function that calls 2 types of api requests to get a bunch of data i need in my app. In the function I make a request for locations, then for each location in the response I make a different request to get details of that specific location. (ex. if request 1 returns 20 locations, my second request is called 20 times, once for each location)
My function code here:
func requestAndCombineGData(location: CLLocation, radius: Int) {
// Clears map of markers
self.mapView.clear()
// Calls 'Nearby Search' request
googleClient.getGooglePlacesData(location: location, withinMeters: radius) { (response) in
print("Made Nearby Search request. Returned response here:", response)
// loops through each result from the above Nearby Request response
for location in response.results {
// Calls 'Place Details' request
self.googleClient.getGooglePlacesDetailsData(place_id: location.place_id) { (detailsResponse) in
print("GMV returned - detailsResponse.result - ", detailsResponse.result)
}
}
}
}
Request functions I reference above are here:
func getGooglePlacesData(location: CLLocation, withinMeters radius: Int, using completionHandler: #escaping (GooglePlacesResponse) -> ()) {
for category in categoriesArray {
let url = googlePlacesNearbyDataURL(forKey: googlePlacesKey, location: location, radius: radius, type: category)
let task = session.dataTask(with: url) { (responseData, _, error) in
if let error = error {
print(error.localizedDescription)
return
}
guard let data = responseData, let response = try? JSONDecoder().decode(GooglePlacesResponse.self, from: data) else {
print("Could not decode JSON response")
completionHandler(GooglePlacesResponse(results:[]))
return
}
if response.results.isEmpty {
print("GC - response returned empty", response)
} else {
print("GC - response contained content", response)
completionHandler(response)
}
}
task.resume()
}
}
func getGooglePlacesDetailsData(place_id: String, using completionHandler: #escaping (GooglePlacesDetailsResponse) -> ()) {
let url = googlePlacesDetailsURL(forKey: googlePlacesKey, place_ID: place_id)
let task = session.dataTask(with: url) { (responseData, _, error) in
if let error = error {
print(error.localizedDescription)
return
}
guard let data = responseData, let detailsResponse = try? JSONDecoder().decode(GooglePlacesDetailsResponse.self, from: data) else {
print("Could not decode JSON response. responseData was: ", responseData)
return
}
print("GPD response - detailsResponse.result: ", detailsResponse.result)
completionHandler(detailsResponse)
}
task.resume()
}
After I get all the data im requesting (or even as the data is coming in) I would like to append it to an #EnvironmentObject (array) I have set up in my SceneDelegate.swift file. Im using the data in multiple places in my app so the #EnvironmentObject serves as a 'source of truth'.
I tried accomplishing this using the code below, but keep getting the error - "Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates."
func requestAndCombineGData(location: CLLocation, radius: Int) {
// Clears map of markers
self.mapView.clear()
// Calls 'Nearby Search' request
googleClient.getGooglePlacesData(location: location, withinMeters: radius) { (response) in
print("Made Nearby Search request. Returned response here:", response)
// loops through each result from the above Nearby Request response
for location in response.results {
// Calls 'Place Details' request
self.googleClient.getGooglePlacesDetailsData(place_id: location.place_id) { (detailsResponse) in
print("GMV returned - detailsResponse.result - ", detailsResponse.result)
// THIS IS WHERE I TRY TO UPDATE MY #ENVIROMETOBJECT
self.venueData.venuesdataarray.append(detailsRespose.result)
}
}
}
}
I believe I need to make sure the requests complete THEN try to update my #EnvironmentObject, but I do not know how to do that.
EDIT - providing my VenueData struct as requested in comments:
struct VenueData : Identifiable {
var id = UUID()
var name : String
var geometry : Location?
var rating : String?
var price_level : String?
var types : [String]?
var formatted_address : String?
var formatted_phone_number : String?
var website : String?
var photo_reference : String?
enum CodingKeysDetails : String, CodingKey {
case geometry = "geometry"
case name = "name"
case rating = "rating"
case price_level = "price_level"
case types = "types"
case opening_hours = "opening_hours"
case formatted_address = "formatted_address"
case formatted_phone_number = "formatted_phone_number"
case website = "website"
}
// Location struct
struct Location : Codable {
var location : LatLong
enum CodingKeys : String, CodingKey {
case location = "location"
}
// LatLong struct
struct LatLong : Codable {
var latitude : Double
var longitude : Double
enum CodingKeys : String, CodingKey {
case latitude = "lat"
case longitude = "lng"
}
}
}
}
class VenueDataArray: ObservableObject {
#Published var venuesdataarray : [VenueData] = [
VenueData(name: "test_name")
]
}
Solution Edit - I tried using this snippet of code within my second api request and it solved my issue, although i do not understand why I need to do this
DispatchQueue.main.async {
self.venueData.venuesdataarray.append(RESPONSE_DETAILS_HERE)
}
Originally I had asked, Does anyone know how I can update my #EnvironmentObject after all the requests complete?
Does anyone know why the snippet I have above makes everything work?
Id just like to understand what im doing and maybe someone could learn something if they find this
I tried using this snippet of code within my second api request and it solved my issue, although i do not understand why I need to do this
DispatchQueue.main.async {
self.venueData.venuesdataarray.append(RESPONSE_DETAILS_HERE)
}
Originally I had asked, Does anyone know how I can update my #EnvironmentObject after all the requests complete?
Does anyone know why the snippet I have above makes everything work? Id just like to understand what im doing and maybe someone could learn something if they find this
There are several things that you cannot successfully do from a background thread. Some of them (like UIKit content changes) do not generate an error, but fail silently which is worse. You have the good fortune to have received a relatively specific error message.
The error message was that you couldn't publish changes from a background thread and needed to do that from the main thread.
Wrapping your append inside "DispatchQueue.main.async" makes that line of code run on the main thread.
That's it.
This could probably have been explained more concisely.
My app is, like oh so many apps, retrieving JSON from an API and converting it using the new Codable protocol in Swift 4. Most of the time, this works fine and as expected. However, sometimes the API will send me unexpected garbage. Incorrect types, arrays with just null inside, that kind of thing.
The problem is that the objects involved can be large and complicated, and when I'm parsing a child object and it fails, the whole object fails, all the way up to the root. I'm including a very simple playground example to illustrate the concept; the actual objects involved are way more complex.
let goodJSON = """
{
"name": "Fiona Glenanne",
"vehicles": [
{
"make": "Saab",
"model": "9-3",
"color": "Black"
},
{
"make": "Hyundai",
"model": "Genesis",
"color": "Blue"
}
]
}
"""
let goodJSONData = goodJSON.data(using: .utf8)!
let badJSON = """
{
"name": "Michael Westen",
"vehicles": {
"make": "Dodge",
"model": "Charger",
"color": "Black"
}
}
"""
let badJSONData = badJSON.data(using: .utf8)!
struct Character: Codable {
let name: String
let vehicles: [Vehicle]
}
struct Vehicle: Codable {
let make: String
let model: String
let color: String
}
do {
let goodCharacter = try JSONDecoder().decode(Character.self, from: goodJSONData)
print(goodCharacter)
} catch {
print(error)
}
do {
let badCharacter = try JSONDecoder().decode(Character.self, from: badJSONData)
print(badCharacter)
} catch DecodingError.typeMismatch(let type, let context) {
print("Got \(type); \(context.debugDescription) ** Path:\(context.codingPath)")
} catch {
print("Caught a different error: \(error)")
}
Output:
Character(name: "Fiona Glenanne", vehicles: [__lldb_expr_20.Vehicle(make: "Saab", model: "9-3", color: "Black"), __lldb_expr_20.Vehicle(make: "Hyundai", model: "Genesis", color: "Blue")])
Got Array<Any>; Expected to decode Array<Any> but found a dictionary instead. ** Path:[CodingKeys(stringValue: "vehicles", intValue: nil)]
vehicles is expected to be an array of objects, but in the badJSON case, it is a single object, which causes the .typeMismatch exception and kills the parsing right there.
What I'm looking for is a way to allow errors like this one to kill the parsing for the child object only and allow parsing of the parent object to continue. I'm looking to do this in a generic fashion, so I don't have to special case each and every object in my app to specifically handle whatever bad data the API delivers. I'm not sure if there even is a solution for this, I haven't had any luck finding anything, but it would sure improve my quality of life if there is. Thanks!
you can try to custom the init(from decoder: Decoder) as suggested in comments it would be something like this,
struct Character: Codable {
let name: String
let vehicles: [Vehicle]
private enum CodingKeys: String, CodingKey { case name, vehicles }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
do {
let vehicle = try container.decode(Vehicle.self, forKey: .vehicles)
vehicles = [vehicle]
} catch DecodingError.typeMismatch {
vehicles = try container.decode([Vehicle].self, forKey: .vehicles)
}
}