Swift decodable propagate userInfo - ios

I have the following models:
struct Shop: Decodable {
let id: Int
let name: String
let products: [Product]
private enum CodingKeys: String, CodingKey {
case id
case name
case products
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int.self, forKey: .id)
self.name = try container.decode(String.self, forKey: .name)
self.products = try container.decode([Product].self, forKey: .products)
}
}
struct Product: Decodable {
let id: Int
let title: String
let productImageURL: String
private(set) var productDetails: ProductDetails
private enum CodingKeys: String, CodingKey {
case id
case title
case productImageList
case productImageDetails
case productDetails
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int.self, forKey: .id)
self.title = try container.decode(String.self, forKey: .title)
self.productDetails = try container.decode(ProductDetails.self, forKey: .productDetails)
guard let serializationType = decoder.userInfo[.serializationType] as? SerializationGenericType else {
throw NetworkError.modelParsingError(message: "Product model missing serialization type")
}
switch serializationType {
case .list:
self.productImageURL = try container.decode(String.self, forKey: .productImageList)
case .details:
self.productImageURL = try container.decode(String.self, forKey: .productImageList)
self.productDetails = try container.decode(ProductDetails.self, forKey: .productDetails)
}
}
}
Of course, the models are much more complex, but this is enough to explain my problem :)
Also, please don't mind the coding keys and the image URL, it's just to show that for different serialization type it initialized differently.
When getting the product details I have:
let decoder = JSONDecoder()
decoder.userInfo[.serializationType] = SerializationGenericType.details
let product = try decoder.decode(Product.self, from: data)
But when I try to pass the serialization type when initializing the Shop in the user info of the JsonDecoder like this:
let decoder = JSONDecoder()
decoder.userInfo[.serializationType] = SerializationGenericType.list
let shop = try decoder.decode(Shop.self, from: data)
In the Product initializer the user info is empty.
My question is how to propagate the serialization type from the Shop to the Product so I can properly decode it using the correct serialization type?

Related

Decoding json with optional fields

I am trying to parse a json string in which some of the keys are not fixed. There are some keys which are related to error and either it will be error or the result data. Followings are the two examples:
{
"ok": true,
"result": {
"code": "694kyH",
"short_link": "shrtco.de\/694kyH",
"full_short_link": "https:\/\/shrtco.de\/694kyH",
"short_link2": "9qr.de\/694kyH",
"full_short_link2": "https:\/\/9qr.de\/694kyH",
"short_link3": "shiny.link\/694kyH",
"full_short_link3": "https:\/\/shiny.link\/694kyH",
"share_link": "shrtco.de\/share\/694kyH",
"full_share_link": "https:\/\/shrtco.de\/share\/694kyH",
"original_link": "http:\/\/google.com"
}
}
{
"ok": false,
"error_code": 2,
"error": "This is not a valid URL, for more infos see shrtco.de\/docs"
}
How will I parse this JSON. I have tried to build my class like following but it is not working:
struct ShortLinkData: Codable {
let ok: Bool
let result: Result?
let errorCode: Int?
let error: String?
private enum CodingKeys : String, CodingKey { case ok, result, errorCode = "error_code", error }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
ok = try container.decode(Bool.self, forKey: .ok)
result = try container.decode(Result.self, forKey: .result)
errorCode = try container.decodeIfPresent(Int.self, forKey: .errorCode)
error = try container.decodeIfPresent(String.self, forKey: .error)
}
}
// MARK: - Result
struct Result: Codable {
let code, shortLink: String
let fullShortLink: String
let shortLink2: String
let fullShortLink2: String
let shortLink3: String
let fullShortLink3: String
let shareLink: String
let fullShareLink: String
let originalLink: String
enum CodingKeys: String, CodingKey {
case code
case shortLink = "short_link"
case fullShortLink = "full_short_link"
case shortLink2 = "short_link2"
case fullShortLink2 = "full_short_link2"
case shortLink3 = "short_link3"
case fullShortLink3 = "full_short_link3"
case shareLink = "share_link"
case fullShareLink = "full_share_link"
case originalLink = "original_link"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
code = try container.decode(String.self, forKey: .code)
shortLink = try container.decode(String.self, forKey: .shortLink)
fullShortLink = try container.decode(String.self, forKey: .fullShortLink)
shortLink2 = try container.decode(String.self, forKey: .shortLink2)
fullShortLink2 = try container.decode(String.self, forKey: .fullShortLink2)
shortLink3 = try container.decode(String.self, forKey: .shortLink3)
fullShortLink3 = try container.decode(String.self, forKey: .fullShortLink3)
shareLink = try container.decode(String.self, forKey: .shareLink)
fullShareLink = try container.decode(String.self, forKey: .fullShareLink)
originalLink = try container.decode(String.self, forKey: .originalLink)
}
}
My parsing code:
let str = String(decoding: data, as: UTF8.self)
print(str)
let shortURL = try? JSONDecoder().decode(ShortLinkData.self, from: data)
return shortURL!
I am always getting nil in shortURL object.
You should split this into several steps in order to avoid to handle all these optionals in your model.
First create a struct that has only those properties that are guaranteed to be there. ok in your case:
struct OKResult: Codable{
let ok: Bool
}
then create one for your error state and one for your success state:
struct ErrorResult: Codable{
let ok: Bool
let errorCode: Int
let error: String
private enum CodingKeys: String, CodingKey{
case ok, errorCode = "error_code", error
}
}
struct ShortLinkData: Codable {
let ok: Bool
let result: Result
}
struct Result: Codable {
let code, shortLink: String
let fullShortLink: String
let shortLink2: String
let fullShortLink2: String
let shortLink3: String
let fullShortLink3: String
let shareLink: String
let fullShareLink: String
let originalLink: String
enum CodingKeys: String, CodingKey {
case code
case shortLink = "short_link"
case fullShortLink = "full_short_link"
case shortLink2 = "short_link2"
case fullShortLink2 = "full_short_link2"
case shortLink3 = "short_link3"
case fullShortLink3 = "full_short_link3"
case shareLink = "share_link"
case fullShareLink = "full_share_link"
case originalLink = "original_link"
}
}
Then you can decode the data:
guard try JSONDecoder().decode(OKResult.self, from: data).ok else{
let errorResponse = try JSONDecoder().decode(ErrorResult.self, from: data)
//handle error scenario
fatalError(errorResponse.error) // or throw custom error or return nil etc...
}
let shortlinkData = try JSONDecoder().decode(ShortLinkData.self, from: data)
Remarks:
Your inits are not necessary.
Never use try? this will hide all errors from you
you would need to wrap this either in a do catch block or make your function throwing and handle errors further up the tree.
Actually there are no optional fields. The server sends two different but distinct JSON strings.
A suitable way to decode both JSON strings is an enum with associated values. It decodes the ok key, then it decodes either the result dictionary or errorCode and error
enum Response : Decodable {
case success(ShortLinkData), failure(Int, String)
private enum CodingKeys : String, CodingKey { case ok, result, errorCode, error }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let ok = try container.decode(Bool.self, forKey: .ok)
if ok {
let result = try container.decode(ShortLinkData.self, forKey: .result)
self = .success(result)
} else {
let errorCode = try container.decode(Int.self, forKey: .errorCode)
let error = try container.decode(String.self, forKey: .error)
self = .failure(errorCode, error)
}
}
}
In ShortLinkData the init method and the CodingKeys are redundant if you specify the convertFromSnakeCase key decoding strategy
struct ShortLinkData: Decodable {
let code, shortLink: String
let fullShortLink: String
let shortLink2, fullShortLink2: String
let shortLink3, fullShortLink3: String
let shareLink, fullShareLink: String
let originalLink: String
}
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let result = try decoder.decode(Response.self, from: data)
switch result {
case .success(let linkData): print(linkData)
case .failure(let code, let message): print("An error occurred with code \(code) and message \(message)")
}
} catch {
print(error)
}

Swift: getting error while decoding using CodingKey

I have created a model and I need to try to add value to one of the CodingKey's case
struct Model {
let name: String
let location: String
let descriptions: String
}
enum CodingKeys: String, CodingKey {
case name = "NAME"
case location = "LOCATION"
case descriptions = "INFO-\(NSLocalizedString("Language", comment: ""))"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
.
.
descriptions = try container.decode(String.self, forKey: .descriptions)
}
Now compiler gives me this error
🛑 Raw value for enum case must be a literal
How can I fix this error?
You don't have to use an enum for CodingKeys. You can also use a struct with static lets. It's just a bit more boilerplate:
struct Model: Decodable {
let name: String
let location: String
let descriptions: String
struct CodingKeys: CodingKey {
let intValue: Int? = nil
let stringValue: String
init(stringValue: String) {
self.stringValue = stringValue
}
init?(intValue: Int) {
return nil
}
static let name = CodingKeys(stringValue: "name")
static let location = CodingKeys(stringValue: "location")
static let description = CodingKeys(stringValue: "INFO-\(NSLocalizedString("Language", comment: ""))")
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
location = try container.decode(String.self, forKey: .location)
descriptions = try container.decode(String.self, forKey: .description)
}
}

Convert dynamic server response in to model class in swift

The following is the server response handling parent structure...
struct ServerResponse<T: Codable>: Codable {
let status: Bool?
let message: String?
let data: T?
enum CodingKeys: String, CodingKey {
case status = "status"
case message = "message"
case data = "data"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
status = try values.decodeIfPresent(Bool.self, forKey: .status)
message = try values.decodeIfPresent(String.self, forKey: .message)
data = try values.decodeIfPresent(T.self, forKey: .data)
}
}
AppUserResponse strucure:
struct AppUserResponse: Codable {
let accessToken : String?
let askForMobileNo : Int?
let tokenType : String?
let user : AppUser?
enum CodingKeys: String, CodingKey {
case accessToken = "access_token"
case askForMobileNo = "ask_for_mobile_no"
case tokenType = "token_type"
case user = "user"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
accessToken = try values.decodeIfPresent(String.self, forKey: .accessToken)
askForMobileNo = try values.decodeIfPresent(Int.self, forKey: .askForMobileNo)
tokenType = try values.decodeIfPresent(String.self, forKey: .tokenType)
user = try values.decodeIfPresent(AppUser.self, forKey: .user)
}
}
struct AppUser: Codable {
let createdAt : String?
let deviceToken : String?
let deviceType : String?
let email : String?
let emailVerifiedAt : String?
let firstName : String?
let id : Int?
let lastName : String?
let mobile_no : String?
let mobileVerified : Int?
let mobileVerifiedAt : String?
let provider : String?
let providerId : String?
let status : Int?
let updatedAt : String?
enum CodingKeys: String, CodingKey {
case createdAt = "created_at"
case deviceToken = "device_token"
case deviceType = "device_type"
case email = "email"
case emailVerifiedAt = "email_verified_at"
case firstName = "first_name"
case id = "id"
case lastName = "last_name"
case mobile_no = "mobile_no"
case mobileVerified = "mobile_verified"
case mobileVerifiedAt = "mobile_verified_at"
case provider = "provider"
case providerId = "provider_id"
case status = "status"
case updatedAt = "updated_at"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
createdAt = try values.decodeIfPresent(String.self, forKey: .createdAt)
deviceToken = try values.decodeIfPresent(String.self, forKey: .deviceToken)
deviceType = try values.decodeIfPresent(String.self, forKey: .deviceType)
email = try values.decodeIfPresent(String.self, forKey: .email)
emailVerifiedAt = try values.decodeIfPresent(String.self, forKey: .emailVerifiedAt)
firstName = try values.decodeIfPresent(String.self, forKey: .firstName)
id = try values.decodeIfPresent(Int.self, forKey: .id)
lastName = try values.decodeIfPresent(String.self, forKey: .lastName)
mobile_no = try values.decodeIfPresent(String.self, forKey: .mobile_no)
mobileVerified = try values.decodeIfPresent(Int.self, forKey: .mobileVerified)
mobileVerifiedAt = try values.decodeIfPresent(String.self, forKey: .mobileVerifiedAt)
provider = try values.decodeIfPresent(String.self, forKey: .provider)
providerId = try values.decodeIfPresent(String.self, forKey: .providerId)
status = try values.decodeIfPresent(Int.self, forKey: .status)
updatedAt = try values.decodeIfPresent(String.self, forKey: .updatedAt)
}
}
TempUserResponse Structure
struct TempUserResponse : Codable {
let askForMobileNo : Int?
let provider : String?
let providerId : String?
let tempUser : TempUser?
enum CodingKeys: String, CodingKey {
case askForMobileNo = "ask_for_mobile_no"
case provider = "provider"
case providerId = "provider_id"
case tempUser = "temp_user"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
askForMobileNo = try values.decodeIfPresent(Int.self, forKey: .askForMobileNo)
provider = try values.decodeIfPresent(String.self, forKey: .provider)
providerId = try values.decodeIfPresent(String.self, forKey: .providerId)
tempUser = try values.decodeIfPresent(TempUser.self, forKey: .tempUser)
}
}
struct TempUser : Codable {
let email : String?
let name : String?
enum CodingKeys: String, CodingKey {
case email = "email"
case name = "name"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
email = try values.decodeIfPresent(String.self, forKey: .email)
name = try values.decodeIfPresent(String.self, forKey: .name)
}
}
PROBLEM
in app we have 2 buttons, 1. FB login and 2. Google Login
When I try login with new user, server return this response(at this time user is not registered so I am getting "tempUser" in "data")
{
"status": true,
"message": "User social email address verified, mobile number unverified!.",
"data": {
"temp_user": {
"name": "dhaval solanki",
"email": "dhaval.sassyinfotech#gmail.com"
},
"provider": "google",
"provider_id": "112620316711299944315",
"ask_for_mobile_no": 1
}
}
When I login with registered user, I am getting following response...
{
"status": true,
"message": "User logged in successfully.",
"data": {
"user": {
"id": 60,
"first_name": "Ankit",
"last_name": "Joshi",
"mobile_no": "9876543211"
},
"access_token": "eyJLCJhbGciOiJIUzI1NR..",
"token_type": "bearer",
"ask_for_mobile_no": 0
}
}
Using following to convert response into model class(I have used Alamofire for api call)
let decoder = JSONDecoder()
do {
let responseData = try decoder.decode(ServerResponse <TempUserResponse>.self, from: serverData)
success(responseData as AnyObject)
} catch {
print("Error = \(error)")
do {
let responseData = try decoder.decode(ServerResponse<AppUserResponse>.self, from: serverData)
success(responseData as AnyObject)
} catch {
failure(error.localizedDescription as AnyObject)
}
}
When I run this code it always goes and try to convert response into ServerResponse<TempUserResponse> even if the response is of type ServerResponse<AppUserResponse>.
So how can I manage both of the response by converting it into respected model class?
The decoding of your JSON response always succeeds in the first try (with TempUserResponse type) because all your properties are optional and decoded using decodeIfPresent<T>(_:forKey:) function.
So, JSONDecoder assumes that the value of the key data in your root object is a TempUserResponse instance but with all of is properties set to nil. (none of the property keys are present in the JSON)
In order to avoid that behavior you can set a property that make sense to you, to be mandatory in TempUserResponse, like for example tempUser:
struct TempUserResponse : Codable {
let askForMobileNo: Int?
let provider: String?
let providerId: String?
let tempUser: TempUser
enum CodingKeys: String, CodingKey {
case askForMobileNo = "ask_for_mobile_no"
case provider = "provider"
case providerId = "provider_id"
case tempUser = "temp_user"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
askForMobileNo = try values.decodeIfPresent(Int.self, forKey: .askForMobileNo)
provider = try values.decodeIfPresent(String.self, forKey: .provider)
providerId = try values.decodeIfPresent(String.self, forKey: .providerId)
tempUser = try values.decode(TempUser.self, forKey: .tempUser)
}
}
That way the decoding will succeed if the tempUser key is present in the JSON and will fail when there is not and fall back to the AppUserResponse decoding.
Update: Another solution would be to merge both structs into one with all of its properties as optionals.
That way your model will be a lot simpler:
struct UserResponse: Codable {
let accessToken: String?
let askForMobileNo: Int?
let tokenType: String?
let user: AppUser?
let provider: String?
let providerId: String?
let tempUser: TempUser?
enum CodingKeys: String, CodingKey {
case accessToken = "access_token"
case askForMobileNo = "ask_for_mobile_no"
case tokenType = "token_type"
case user = "user"
case provider = "provider"
case providerId = "provider_id"
case tempUser = "temp_user"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
accessToken = try values.decodeIfPresent(String.self, forKey: .accessToken)
askForMobileNo = try values.decodeIfPresent(Int.self, forKey: .askForMobileNo)
tokenType = try values.decodeIfPresent(String.self, forKey: .tokenType)
user = try values.decodeIfPresent(AppUser.self, forKey: .user)
provider = try values.decodeIfPresent(String.self, forKey: .provider)
providerId = try values.decodeIfPresent(String.self, forKey: .providerId)
tempUser = try values.decodeIfPresent(TempUser.self, forKey: .tempUser)
}
}
Note: Structs that not changed are not included.
The decoding code will look like this:
let decoder = JSONDecoder()
do {
let responseData = try decoder.decode(ServerResponse<UserResponse>.self, from: Data(jsonString.utf8))
print(responseData)
} catch {
print(error)
}
and you can just check if user or tempUser property exist to determine your case.
You can have one extra layer class like
struct FailableResponse <T:Codable,E:Codable> : Codable {
var success:T?
var failure:E?
public init(from decoder:Decoder) throws {
let singleValue = try decoder.singleValueContainer()
success = try singleValue.decode(T.self)
failure = try singleValue.decode(E.self)
}
}
And Pass Your correct response and wrong response as T and E
Note: Not tested in xcode but should work
Replace TempUserResponse model class
struct TempUserResponse : Codable {
let askForMobileNo : Int?
let provider : String?
let providerId : String?
let tempUser : TempUser?
var appUser: AppUser? = nil
enum CodingKeys: String, CodingKey {
case askForMobileNo = "ask_for_mobile_no"
case provider = "provider"
case providerId = "provider_id"
case tempUser = "temp_user"
case appUser = "user"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
askForMobileNo = try values.decodeIfPresent(Int.self, forKey: .askForMobileNo)
provider = try values.decodeIfPresent(String.self, forKey: .provider)
providerId = try values.decodeIfPresent(String.self, forKey: .providerId)
tempUser = try values.decodeIfPresent(TempUser.self, forKey: .tempUser)
if tempUser == nil{
appUser = try values.decodeIfPresent(AppUser.self, forKey: .appUser)
}
}
}
Response in model class
do {
let responseData = try decoder.decode(ServerResponse <TempUserResponse>.self, from: serverData)
print("Response Success: \(responseData)")
if let tempUser = responseData.data?.tempUser{
print("TempUser: \(tempUser)")
}
if let appuser = responseData.data?.appUser{
print("AppUser : \(appuser)")
}
} catch {
print("Error = \(error)")
}

Saving codable struct in Userdefaults returns nil values upon fetching

Saving data in defaults:
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(user) {
let defaults = UserDefaults.standard
defaults.set(encoded, forKey: DefaultKeys.user)
defaults.synchronize()
}
fetching data but getting nil keys of struct.
let defaults = UserDefaults.standard
if let savedPerson = defaults.object(forKey: DefaultKeys.user) as? Data {
let decoder = JSONDecoder()
if let loadedPerson = try? decoder.decode(UserModel.self, from: savedPerson) {
return loadedPerson
}
}
I tried to google the approaches everyone suggest the same to save custom object in userdefaults, but the approach is not working for me.Given below is my struct model which conform Codable as well.
struct UserModel : Codable {
var userDetails : UserDetail?
var status : Bool?
var message : String?
enum CodingKeys: String, CodingKey {
case userDetails = "userDetails"
case status = "status"
case message = "message"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
userDetails = try UserDetail(from: decoder)
status = try values.decodeIfPresent(Bool.self, forKey: .status)
message = try values.decodeIfPresent(String.self, forKey: .message)
}
}

Store Decodable struct to Userdefaults

I have a Decodable struct like so...
struct BestWishes: Decodable {
private enum CodingKeys : String, CodingKey {
case customerID = "customer_id"
case birthDate = "birth_date"
case type = "type"
case customerName = "customer_name"
case mobileNo = "mobile_number"
}
let customerID: Int
let date: String
let type: String
let customerName : String
let mobileNumber: Int
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
customerID = try container.decode(Int.self, forKey: .customerID)
type = try container.decode(String.self, forKey: .type)
if type == "anniversary_date" {
date = try container.decode(String.self, forKey: .anniversaryDate)
} else {
date = try container.decode(String.self, forKey: .birthDate)
}
customerName = try container.decode(String.self, forKey: .customerName)
mobileNumber = try container.decode(Int.self, forKey: .mobileNo)
}
}
And all the data within this is stored in an array like so..
var bestWishesArr = [BestWishes]()
self.bestWishesArr.append(contentsOf: result.bestWishes)
Now I would like to store bestWishesArr to Userdefaults. Not sure how to achieve that...
Add the second half of the Codable protocol. To be compatible with the init method the date property is saved differently depending on type
struct BestWish: Codable {
private enum CodingKeys : String, CodingKey {
case customerID = "customer_id"
case birthDate = "birth_date"
case anniversaryDate = "anniversary_date"
case type
case customerName = "customer_name"
case mobileNumber = "mobile_number"
}
let customerID: Int
let date: String
let type: String
let customerName : String
let mobileNumber: Int
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
customerID = try container.decode(Int.self, forKey: .customerID)
type = try container.decode(String.self, forKey: .type)
if type == "anniversary_date" {
date = try container.decode(String.self, forKey: .anniversaryDate)
} else {
date = try container.decode(String.self, forKey: .birthDate)
}
customerName = try container.decode(String.self, forKey: .customerName)
mobileNumber = try container.decode(Int.self, forKey: .mobileNumber)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(customerID, forKey: .customerID)
try container.encode(type, forKey: .type)
if type == "anniversary_date" {
try container.encode(date, forKey: .anniversaryDate)
} else {
try container.encode(date, forKey: .birthDate)
}
try container.encode(customerName, forKey: .customerName)
try container.encode(mobileNumber, forKey: .mobileNumber)
}
}
Then encode the array and write the Data object to UserDefaults
do {
let jsonData = try JSONEncoder().encode(bestWishesArr)
UserDefaults.standard.set(jsonData, forKey:"bestWishes")
} catch { print(error) }

Resources