I want to use the same model struct for sending parameters and receiving JSON response from an API.
I want to send email and password and receive JSON response message.
But when I declare a property referencing another model in my user model, I am forced to provide a value.
My API does not return the parameters email and password, it simply returns an array if login is unsuccessful and returns the user info on successful login.
this is the json response on error :
Optional({
errors = {
msg = "USER_DOES_NOT_EXIST";
};
this is my model I used to post:
struct LoginUser: Codable {
let email: String?
let password: String?
}
this is the response on successful login:
Optional({
token = 267e6a9d5a128fb1f44e670fcd89793af50fa9a831e6ae7dc2f0592b508bd224a71290fbdf1619cf52ed0f2c034b26383b915343f3822a52e1386c042484744b71811f80d3cb663fc76a6cc74d4866737421e3b9d62e35b415c0f7c385e5c70d472a5facf7f0101165d321c35eb20ae5f8bb32f06120e66a42de47c79a7587a2aa7f81f35c3821b9418e0c9142a7ec2b67b9755d3e17753dd8f1cdf3f71c0816627e2be26485f9b50ee1ad96a867856f0de736963c5ff59e9e37e92d5f3386f7;
user = {
"_id" = 5e52c0c5cf65d33726a98590;
email = "test7#gmail.com";
"first_name" = gagz;
"last_name" = bhullar;
verified = 0;
};
})
I want to modify my loginUser model such that it can both receive and send data.
is it possible?
if not, why?
For an API which returns two different models on success and failure I highly recommend to use an enum with associated values. The benefit is you get rid of the tons of optionals (and if let expressions) and you can use a descriptive switch for the success and failure cases.
enum Response : Decodable {
case success(UserResponse)
case failure(ErrorResponse)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
let userData = try container.decode(UserResponse.self)
self = .success(userData)
} catch DecodingError.typeMismatch {
let errorData = try container.decode(ErrorResponse.self)
self = .failure(errorData)
}
}
}
And these are the corresponding structs
struct ErrorResponse: Decodable {
let errors: [String:String]
}
struct UserResponse: Decodable {
let token : String
let user: User
}
struct User: Decodable {
private enum CodingKeys : String, CodingKey {
case id = "_id"
case email
case firstName = "first_name"
case lastName = "last_name"
case verified
}
let id, email, firstName, lastName : String
let verified : Int // maybe Bool
}
To send only two key-value pairs an extra struct and Codable is overkill. Create a temporary [String:String] dictionary and encode it with JSONSerialization or create the JSON Data directly for example
func jsonSendData(email: String, password: String) -> Data {
let jsonString =
"""
{"email":"\(email)","password":"\(password)"}
"""
return Data(jsonString.utf8)
}
Although it is recommended to create separate model for handling the failure and success cases, still if you want to create a single model, you can do it like so,
struct LoginUser: Codable {
let email, password, token, id, firstName, lastName: String?
let verified: Int?
let errorMessage: String?
enum CodingKeys: String, CodingKey {
case email, token, _id, firstName, lastName, verified, user
case errors, msg
case password
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
token = try container.decodeIfPresent(String.self, forKey: .token)
if container.contains(.user) {
let user = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .user)
id = try user.decodeIfPresent(String.self, forKey: ._id)
email = try user.decodeIfPresent(String.self, forKey: .email)
firstName = try user.decodeIfPresent(String.self, forKey: .firstName)
lastName = try user.decodeIfPresent(String.self, forKey: .lastName)
verified = try user.decodeIfPresent(Int.self, forKey: .verified)
errorMessage = nil
} else if container.contains(.errors) {
let errors = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .errors)
errorMessage = try errors.decodeIfPresent(String.self, forKey: .msg)
id = nil
email = nil
firstName = nil
lastName = nil
verified = nil
} else {
id = nil
email = nil
firstName = nil
lastName = nil
verified = nil
errorMessage = nil
}
password = nil
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(email, forKey: .email)
try container.encode(password, forKey: .password)
//add the keys that you want to send in the request...
}
}
Since, you haven't added the the JSON that you want to send in the request, I've assumed that you're only sending email and password in the request. You can add more in the above encode(to:) method.
Related
I'm fetching a document from Firestore, and I'm marshalling it into a struct using the DocumentSnapshot.data(as:) function from FirebaseFirestoreSwift. I want to be able to set default values for certain fields that may or may not exist on the document (like privateProfile below).
However, when the prop doesn't exist on the document, an error is thrown from DocumentSnapshot.data(as:). Is there a way to get the below example to work or do I have to get creative (e.g. custom decoder initialiser)?
struct MyUser: Codable {
#DocumentID var id: String?
var username: String?
var privateProfile: Bool = true // This is what's causing the issue.
// var privateProfile: Bool? // Would work, but then I have to handle nil values everywhere.
}
userDataListener = db.collection(usersCollection).document(userID)
.addSnapshotListener { (querySnapshot, error) in
if let querySnapshot = querySnapshot {
do {
if let u = try querySnapshot.data(as: MyUser.self) {
self.user = u
}
} catch {
// Handle error...
}
}
}
With help from #Paulw11 we figured that the nicest way while keeping the default values in the field declarations would be writing a custom initialiser like so
struct MyUser: Codable {
#DocumentID var id: String?
var username: String?
var privateProfile: Bool = true
enum CodingKeys: String, CodingKey {
case id = "id"
case username = "username"
case privateProfile = "private_profile"
}
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
self.id = try c.decodeIfPresent(String.self, forKey: .id)
self.username = try c.decodeIfPresent(String.self, forKey: .username)
self.privateProfile = try c.decodeIfPresent(Bool.self, forKey: .privateProfile) ?? self.privateProfile
}
}
It's a lot to write when there are many fields though.
I have the following object
struct Properties: Decodable {
var id: String?
var value: String?
var color: String?
}
In the first request to server I get the following response
{
"id":"1",
"color":"red"
}
And after another request I get
{
"id":"1", // the id of the object props is meant for
"props":{
"value":"my value" // I can get any property here
}
}
After the two requests I should have the object with all properties set
By now I decode the second request as following
struct SetAttr: Decodable {
let id: String
let props: [String : Any]
enum SetAttrCodingKeys: String, CodingKey {
case id
case props
}
init(from decoder: Decoder) throws {
let container = try! decoder.container(keyedBy: SetAttrCodingKeys.self)
props = try! container.decode([String : Any].self, forKey: .props)
id = try! container.decode(String.self, forKey: .id)
}
}
But I do not know how to parse props dictionary and set the properties on the first object. I am willing to use a decoding library, but I did not find any that can do this
EDIT:
This is how I tried to set the properties from dictionary, but the solution is not scalable
var myObject: Properties
properties = setAttr.props // [String:Any]
let keys = properties.keys
keys.forEach { key in
if let value = properties[key] {
switch key {
case "value":
myObject.value = value as? String
case "color":
myObject.color = value as? String
default:
break
}
}
}
Just use JSONSerialization which parses whatever you throw at it into arrays and dictionaries. That frees you from all the problems you have with strangely formatted JSON.
For example, the second request will be parsed as a dictionary with two keys "id" and "props", and "props" has a value which is again a dictionary with one key "value" and a value "my value".
And please stop using try! That will cause your app to crash instantly if any input is not expected. Unexpected inputs should be handled, not lead to a crash.
There are various way to do this, but one possible way could be something like this:
struct SecAttr: Decodable {
let id: String
var props: Properties?
private enum CodingKeys: String, CodingKey {
case id
case props
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
if let props = try container.decodeIfPresent(Properties.self, forKey: .props) {
self.props = props
} else {
// decode Properties from the same object
self.props = try Properties(from: decoder)
}
}
}
struct Properties: Decodable {
var value: String?
var color: String?
mutable update(from props: Properties) {
value = props.value ?? value
color = color.value ?? color
}
}
Now you can decode your original object and after getting updated properties, just update them on the original.
when a user login, I get an answer from server that contains an array like this:
{"answer":{"id":26,"username":"iosuser","address":"iosios","phone_number":"123123123","email":"gmail#gmail.com","created_at":"2019-02-02 12:42:50","updated_at":"2019-02-02 21:40:29","provider_id":"1","is_loged_in":1,"user_id":"24","user_type":1}}
How to save this array's data in userdefaults and use it later for example in a profile page?
Any help much appreciated.
First don't recommend saving it in userDefault , you can use coredata / realm , and your content is a dictionary that contains an array , but you can do
struct Root: Codable {
let answer: Answer
}
struct Answer: Codable {
let id: Int
let username, address, phoneNumber, email: String
let createdAt, updatedAt, providerID: String
let isLogedIn: Int
let userID: String
let userType: Int
enum CodingKeys: String, CodingKey {
case id, username, address
case phoneNumber = "phone_number"
case email
case createdAt = "created_at"
case updatedAt = "updated_at"
case providerID = "provider_id"
case isLogedIn = "is_loged_in"
case userID = "user_id"
case userType = "user_type"
}
}
UserDefaults.standard.set(data,forKey:"AnyKey")
then read the data and
let res = try? JSONDecoder().decode(Root.self,from:data)
if you need to strip the array only then
do {
let tr = try JSONSerialization.jsonObject(with:data) as! [String:Any]
let arr = tr["answer"] as! [Any]
let ansData = try JSONSerialization.data(withJSONObject:arr, options:[])
UserDefaults.standard.set(ansData,forKey:"AnyKey")
} catch { print(error) }
After that read it like
guard let data = UserDefaults.standard.data(forKey:"AnyKey") else { return }
let res = try? JSONDecoder().decode([Answer].self,from:data)
From the server I have a big JSON returned that looks something like this:
{
"id": "123",
"status": "ok",
"person": {
"administration": {
"name": "John"
}
},
"company": {
"name": "Test"
}
}
I have a struct:
struct Info: Decodable, Object {
let id: String
let status: String
let personName: String
let companyName: String
}
It conforms to Decodable protocol and also is a Object (Realm entity).
My question is: Am I able somehow to decode the name of the person in personName? Something like person.administration.name.
I want the end Realm Object, to be a flat one and mostly all of the fields are strings.
Should I create separate structs for Person/Company without being Realm Objects and in decode method to set the corresponding value to "personName"?
let personName: String = try container.decode((Person.Administration.name).self, forKey: .personName)
You can simply use containers to decode nested data with Decodable, i.e.
struct Info: Decodable {
let id: String
let status: String
let personName: String
let companyName: String
enum CodingKeys: String, CodingKey {
case id, status
case person, administration
case company
case name
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decode(String.self, forKey: .id)
status = try values.decode(String.self, forKey: .status)
//Decoding personName
let person = try values.nestedContainer(keyedBy: CodingKeys.self, forKey: .person)
let administration = try person.nestedContainer(keyedBy: CodingKeys.self, forKey: .administration)
personName = try administration.decode(String.self, forKey: .name)
//Decoding companyName
let company = try values.nestedContainer(keyedBy: CodingKeys.self, forKey: .company)
companyName = try company.decode(String.self, forKey: .name)
}
}
Example:
I've decoded the JSON you provided above, i.e.
if let data = json.data(using: .utf8) {
let info = try? JSONDecoder().decode(Info.self, from: data)
print(info)
}
The output it gives is:
(id: "123", status: "ok", personName: "John", companyName: "Test")
You can separate out the CodingKeys for all the different levels as per your wish. I kept them at the same level for simplicity.
Suggestion: Try using the optional types with Codable. This is because the API response can be unexpected. And if you don't get any expected key-value pair, you might end up getting a nil while creating the object.
It is best practice to separate transport types you're parsing your JSON into and types to represent object in the storage.
But if you want to use this combined types you should do something like this:
struct Info: Decodable {
let id: String
let status: String
let personName: String
let companyName: String
// JSON root keys
private enum RootKeys: String, CodingKey {
case id, status, person, company
}
// Keys for "person" nested "object"
private enum PersonKeys: String, CodingKey {
case administration
}
// Keys for "administration" and "company"
private enum NamedKeys: String, CodingKey {
case name
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: RootKeys.self)
self.id = try container.decode(String.self, forKey: .id)
self.status = try container.decode(String.self, forKey: .status)
let personContainer = try container.nestedContainer(keyedBy: PersonKeys.self, forKey: .person)
let administrationContainer = try personContainer.nestedContainer(keyedBy: NamedKeys.self, forKey: .administration)
self.personName = try administrationContainer.decode(String.self, forKey: .name)
let companyContainer = try container.nestedContainer(keyedBy: NamedKeys.self, forKey: .company)
self.companyName = try companyContainer.decode(String.self, forKey: .name)
}
}
I separated keys into three different CodingKey types for some type safety, and to prevent accidental mixup.
(This is a follow-up from this question: Using Decodable protocol with multiples keys.)
I have the following Swift code:
let additionalInfo = try values.nestedContainer(keyedBy: UserInfoKeys.self, forKey: .age)
age = try additionalInfo.decodeIfPresent(Int.self, forKey: .realage)
I know that if I use decodeIfPresent and don't have the property it will still handle it correctly if it's an optional variable.
For example the following JSON works to parse it using the code above.
{
"firstname": "Test",
"lastname": "User",
"age": {"realage": 29}
}
And the following JSON works as well.
{
"firstname": "Test",
"lastname": "User",
"age": {"notrealage": 30}
}
But the following doesn't work.
{
"firstname": "Test",
"lastname": "User"
}
How can I make all 3 examples work? Is there something similar to decodeIfPresent for nestedContainer?
You can use the following KeyedDecodingContainer function:
func contains(_ key: KeyedDecodingContainer.Key) -> Bool
Returns a Bool value indicating whether the decoder contains a value associated with the given key. The value associated with the given key may be a null value as appropriate for the data format.
For instance, to check if the "age" key exists before requesting the corresponding nested container:
struct Person: Decodable {
let firstName, lastName: String
let age: Int?
enum CodingKeys: String, CodingKey {
case firstName = "firstname"
case lastName = "lastname"
case age
}
enum AgeKeys: String, CodingKey {
case realAge = "realage"
case fakeAge = "fakeage"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.firstName = try values.decode(String.self, forKey: .firstName)
self.lastName = try values.decode(String.self, forKey: .lastName)
if values.contains(.age) {
let age = try values.nestedContainer(keyedBy: AgeKeys.self, forKey: .age)
self.age = try age.decodeIfPresent(Int.self, forKey: .realAge)
} else {
self.age = nil
}
}
}
I had this issue and I found this solution, just in case is helpful to somebody else:
let ageContainer = try? values.nestedContainer(keyedBy: AgeKeys.self, forKey: .age)
self.age = try ageContainer?.decodeIfPresent(Int.self, forKey: .realAge)
If you have an optional container, using try? values.nestedContainer(keyedBy:forKey) you don't need to check if the container exist using contains(.
Can you try pasting your sample JSON into quicktype to see what types it infers? Based on your question, I pasted your samples and got:
struct UserInfo: Codable {
let firstname: String
let age: Age?
let lastname: String
}
struct Age: Codable {
let realage: Int?
}
Making UserInfo.age and Age.realage optionals works, if that's what you're trying to accomplish.