Reputation: 7521
Here's my code:
class LoginUserResponse : Codable {
var result: String = ""
var data: LoginUserResponseData?
var mess: [String] = []
}
public class LoginUserResponseData : Codable {
var userId = "0"
var name = ""
}
Now, calling the server API I'm parsing response like this (using Stuff library to simplify parsing):
do {
let loginUserResponse = try LoginUserResponse(json: string)
} catch let error {
print(error)
}
When I enter the correct password I'm getting an answer like this:
{"result":"success","data":{"userId":"10","name":"Foo"},"mess":["You're logged in"]}
This is fine, the parser is working correctly.
While providing wrong password gives the following answer:
{"result":"error","data":{},"mess":["Wrong password"]}
In this situation, the parser is failing. It should set data to nil, but instead, it tries to decode it to the LoginUserResponseData object.
I'm using the same approach on Android using retrofit and it works fine. I rather don't want to make all fields as optional.
Is there a way to make parser treat empty json {} as nil? Or make LoginUserResponseData as non-optional and it'll just have default values? I know I can create a custom parser for this, but I have tons of requests like this and it'll require too much additional work.
Upvotes: 10
Views: 26194
Reputation: 1145
First time I was facing this, normally backend would send nil but I was receiving empty data. Just make the data inside User data optional and it will work out of the box.
Looks like tedious to be unwrapping when needed, but if you have your API Layer, and your Business Model Layer which you would build from your API object with the exact data that you need is totally fine.
struct LoginUserResponse : Codable {
let result: String
let data: LoginUserResponseData?
let mess: [String] = []
}
struct LoginUserResponseData : Codable {
let userId: String?
let name: String?
}
Upvotes: 1
Reputation: 396
Wow, okay this doesn't work at all. Sorry.
I came across this post a few years late, but there are certain problems with each of the solutions. Changing the JSON is potentially impractical, silencing the error with
try?
has the potential to ignore other, potentially legitimate errors.Here's a proposed solution that I have used in a project via extending
KeyedDecodingContainer
: ``` fileprivate extension KeyedDecodingContainer { private struct EmptyObject: Decodable {}func decodePossibleEmptyObject<T: Decodable>(_ key: K) throws -> T? { if let _ = try? decode(EmptyObject.self, forKey: key) { return nil } return try self.decode(T.self, forKey: key) } } ```
Creating an
EmptyObject
representation allowstry?
to only succeed if, in fact, the object is an empty object. Otherwise, the decoder will continue to decode the object as requested, with errors falling through the method.The biggest downside is that this requires a custom
init(from: Coder)
.
Upvotes: 0
Reputation: 2316
As easy as that !
class LoginUserResponse : Codable {
var result: String = ""
var data: LoginUserResponseData?
var mess: [String] = []
private enum CodingKeys: String, CodingKey {
case result, data, mess
}
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
result = try values.decode(String.self, forKey: .result)
mess = try values.decode([String].self, forKey: .mess)
data = try? values.decode(LoginUserResponseData.self, forKey: .data)
}
}
public class LoginUserResponseData : Codable {
var userId = "0"
var name = ""
}
let str = "{\"result\":\"success\",\"data\":{\"userId\":\"10\",\"name\":\"Foo\"},\"mess\":[\"You're logged in\"]}"
let str2 = "{\"result\":\"error\",\"data\":{},\"mess\":[\"Wrong password\"]}"
let decoder = JSONDecoder()
let result = try? decoder.decode(LoginUserResponse.self, from: str.data(using: .utf8)!)
let result2 = try? decoder.decode(LoginUserResponse.self, from: str2.data(using: .utf8)!)
dump(result)
dump(result2)
Upvotes: 8
Reputation: 285069
My recommendation is to decode result
as enum
and to initialize data
on success.
struct LoginUserResponse : Decodable {
enum Status : String, Decodable { case success, error }
private enum CodingKeys: String, CodingKey { case result, data, mess }
let result : Status
let data : UserData?
let mess : [String]
init(from decoder: Decoder) throws
{
let values = try decoder.container(keyedBy: CodingKeys.self)
result = try values.decode(Status.self, forKey: .result)
mess = try values.decode([String].self, forKey: .mess)
switch result {
case .success: data = try values.decode(UserData.self, forKey: .data)
case .error: data = nil
}
}
}
public struct UserData : Decodable {
let userId : String
let name : String
}
Upvotes: 4
Reputation: 7521
Seems it's not possible to treat {} as null, so instead I've created a simple tool to "fix" the API response:
extension String {
func replaceEmptyJsonWithNull() -> String {
return replacingOccurrences(of: "{}", with: "null")
}
}
Other ways are described by @Vitaly Gozhenko and should be used, but I cannot change the server API nor want to write full custom decoder, because this one case.
Upvotes: 0
Reputation: 639
This is what your implementation of init(from: Decoder)
should look like.
Note: You should consider changing LoginUserResponse
from a class to a struct, since all it does is store values.
struct LoginUserResponse: Codable {
var result: String
var data: LoginUserResponseData?
var mess: [String]
init(from decoder: Decoder) throws
{
let values = try decoder.container(keyedBy: CodingKeys.self)
result = try values.decode(String.self, forKey: .result)
mess = try values.decode([String].self, forKey: .mess)
if let d = try? values.decode(LoginUserResponseData.self, forKey: .data) {
data = d
}
}
}
Upvotes: 3
Reputation: 9354
This is because {}
is an empty object but not nil.
You have 2 options:
null
instead of {}
for data
keyinit(from: Decoder)
and handle this case manually Upvotes: 3