Error decoding data with Codable even though parameters are optional - ios

I have the following struct:
struct Recipe: Codable {
#DocumentID var id: String?
var minutes: Int?
init(id: String, minutes: Int) {
self.id = id
self.minutes = minutes
}
init(from decoder: Decoder) throws {
enum DecodingKeys: CodingKey {
case minutes
}
let container = try decoder.container(keyedBy: DecodingKeys.self)
minutes = try container.decode(Int.self, forKey: .minutes)
}
}
I'm decoding from a source where minutes is null, so I get the following error message:
Error parsing response data: keyNotFound(DecodingKeys(stringValue: "minutes", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key DecodingKeys(stringValue: \"minutes\", intValue: nil) (\"minutes\").", underlyingError: nil))
However, I thought marking minutes as optional in struct Recipe was sufficient to handle this case? Is there something else I need to implement?

When you implement init(from:) manually you need to use the decodeIfPresent(_:forKey:) variant for optional properties. The decode(_:forKey:) method throws an error if a nullable field in the JSON data is absent whereas the decodeIfPresent(_:forKey:) method just returns nil.
Try this:
init(from decoder: Decoder) throws {
enum DecodingKeys: CodingKey {
case minutes
}
let container = try decoder.container(keyedBy: DecodingKeys.self)
minutes = try container.decodeIfPresent(Int.self, forKey: .minutes)
}

Related

What's the proper syntax of mapping/filtering a data-stream element to convert its data type?

Scenario: Data stream that contains an item that has changed from an Int to String type causing the JSON parser to crash.
Result: Subscriber 'sink' crashed with data type not matching the original receiving type via JSON parser.
Goal: to convert the Int values to String to have a consistent stream for a successful parsing.
Here's a snippet of the data stream that has caused the crash:
...
{
"city": "אלון שבות",
"sickCount": 124,
"actualSick": 15,
"verifiedLast7Days": " 11-14 ",
"testLast7Days": 699,
"patientDiffPopulationForTenThousands": 47
},
{
"city": "סייד (שבט)",
"sickCount": " קטן מ-15 ",
"actualSick": " קטן מ-15 ",
"verifiedLast7Days": " 0 ",
"testLast7Days": 17,
"patientDiffPopulationForTenThousands": 4
},
...
Here's the error via console:
CodingKeys(stringValue: "sickCount", intValue: nil)], debugDescription: "Expected to decode Int but found a string/data instead.", underlyingError: nil))
Here's the code:
func getData() {
let str = "https://disease.sh/v3/covid-19/gov/Israel"
let url = URL(string: str)!
let remoteDataPublisher = URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.receive(on: DispatchQueue.main)
.decode(type: IsraelDataElement.self, decoder: JSONDecoder())
remoteDataPublisher
.eraseToAnyPublisher()
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("{IsraelModel} Publisher Finished")
case let .failure(anError):
Swift.print("\nIsrael - Received error: #function", anError)
}
}, receiveValue: { someData in
self.add_UUID(origData: someData)
print(someData)
}).store(in: &cancellables)
}
I wasn't sure if what I was suggesting in comments wasn't clear, so here's an example of what I had in mind.
If the object you're trying to decode is:
struct IsraelDataElement {
let city: String
let sickCount: String,
let actualSick: String,
let verifiedLast7Days: String,
let testLast7Days: Int,
let patientDiffPopulationForTenThousands: Int
}
then you can manually decode it, converting renegade Ints to Strings:
extension IsraelDataElement: Decodable {
private enum CodingKeys: CodingKey {
case city, sickCount, actualSick, verifiedLast7Days //... etc
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.city = try container.decode(String.self, forKey: .city)
do {
self.sickCount = try String(container.decode(Int.self, forKey: .sickCount))
} catch DecodingError.typeMismatch {
self.sickCount try container.decode(String.self, forKey: .sickCount)
}
// and so on for other properties
}
}
Then, no further changes are needed in your Combine chain.
Swift strongly typed so it will not coerce the types for you. If you genuinely mean for the types to be heterogenous then idiomatically you should use an enum to represent that this thing is either an Int or String. This is the only way you can actually round trip the data and preserver the type. For instance:
struct Element: Decodable {
let city: String
let sickCount: Either<String, Int>
let actualSick: Either<String, Int>
let verifiedLast7Days: String
let testLast7Days: Int
let patientDiffPopulationForTenThousands: Int
}
enum Either<Left: Decodable, Right: Decodable>: Decodable {
case left(Left)
case right(Right)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let x = try? container.decode(Left.self) {
self = .left(x)
} else if let x = try? container.decode(Right.self) {
self = .right(x)
} else {
throw DecodingError.typeMismatch(Self.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for \(String(describing:Self.self))"))
}
}
}
If this is just a case of your backend developers being sloppy then convert the type yourself and you can do so generically so you can easily fix their data problems everywhere in your app:
struct Element: Decodable {
let city: String
let sickCount: Int
let actualSick: Int
enum CodingKeys: CodingKey {
case city, sickCount, actualSick
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.city = try container.decode(String.self, forKey: .city)
self.sickCount = try container.decodeAndCoerce(to: Int.self, from: String.self, conversion: Int.init(_:), forKey: .sickCount)
self.actualSick = try container.decodeAndCoerce(to: Int.self, from: String.self, conversion: Int.init(_:), forKey: .actualSick)
}
}
extension KeyedDecodingContainer {
func decodeAndCoerce<Target: Decodable, Source: Decodable>(to: Target.Type, from: Source.Type, conversion: #escaping (Source) -> Target?, forKey key: Key) throws -> Target {
guard let value = (try? decode(Target.self, forKey: key)) ?? ((try? decode(Source.self, forKey: key)).flatMap(conversion)) else {
throw DecodingError.typeMismatch(Target.self, DecodingError.Context(codingPath: codingPath, debugDescription: "Expected \(String(describing: Target.self))"))
}
return value
}
}
Aren't generics fun?

Decoding JSON and map it to existing object Swift

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.

Best approach for handing Response JSON data structures?

I have built a backend and am working on a front end client, currently, a login response from my client returns:
{
"user": {
"email": "fdsa#fdsafsdfa.com",
"token": "eyJhbGciOiJIUzI"
}
}
This is presenting me an issue where I cant simply decode to a user object, Im having to do the following 2 layers for everything:
struct User: Codable {
let email: String
let token: String
}
struct UserResponseData: Codable {
let user: User
}
Is there a more effective way to directly access the values and constrict the object? Perhaps my editing the user json parent to something more generic like data then sit the user inside that? I am unsure...
My client looks like the below, if that helps inform an improved architecture:
class APIClient {
static func signup(request: SignupRequestData, completion: #escaping (Result<UserResponseData>) -> Void) {
performRequest(route: APIRouter.signup(request), completion: completion)
}
#discardableResult
private static func performRequest<T:Decodable>(route: APIRouter,
decoder: JSONDecoder = JSONDecoder(),
completion:#escaping (Result<T>)->Void) -> DataRequest {
return AF.request(route).responseDecodable (decoder: decoder){ (response: DataResponse<T>) in
completion(response.result)
}
}
}
Appreciate some assistance in a better suited structure moving forward so I dont have to keep unwrapping a parent to get to the values needed for the client
One fairly simple approach is to wrap this up into a general-purpose Response type that assumes the first key is always the right one.
struct AnyStringKey: CodingKey {
var stringValue: String
init?(stringValue: String) { self.stringValue = stringValue }
var intValue: Int?
init?(intValue: Int) { return nil }
}
struct Response<Payload: Decodable>: Decodable {
let payload: Payload
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: AnyStringKey.self)
guard let key = container.allKeys.first else {
throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath,
debugDescription: "Missing payload key"))
}
self.payload = try container.decode(Payload.self, forKey: key)
}
}
let user = try JSONDecoder().decode(Response<User>.self, from: json).payload
It's possible to make this more advanced, and check that the key matches your expectation, but this is probably sufficient for your situation.
This approach moves some work into the caller (calling .payload). You can get rid of that at the cost of moving some of the work into the decoded type with a protocol that handles extracting sub-keys.
protocol LayerDecodable: Decodable {
associatedtype CodingKeys: CodingKey
init(from container: KeyedDecodingContainer<CodingKeys>) throws
}
extension LayerDecodable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: AnyStringKey.self)
guard let key = container.allKeys.first else {
throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath,
debugDescription: "Missing payload key"))
}
try self.init(from: container.nestedContainer(keyedBy: CodingKeys.self, forKey: key))
}
}
But with this, you need to implement the decoding by hand.
struct User: LayerDecodable {
let email: String
let token: String
enum CodingKeys: CodingKey {
case email, token
}
init(from container: KeyedDecodingContainer<CodingKeys>) throws {
self.email = try container.decode(String.self, forKey: .email)
self.token = try container.decode(String.self, forKey: .token)
}
}
The benefit is that the caller is now clean:
let user = try JSONDecoder().decode(User.self, from: json)

Swift codable, Default Value to Class property when key missing in the JSON

As you know Codable is new stuff in swift 4, So we gonna move to this one from the older initialisation process for the Models. Usually we use the following Scenario
class LoginModal
{
let cashierType: NSNumber
let status: NSNumber
init(_ json: JSON)
{
let keys = Constants.LoginModal()
cashierType = json[keys.cashierType].number ?? 0
status = json[keys.status].number ?? 0
}
}
In the JSON cashierType Key may missing, so we giving the default Value as 0
Now while doing this with Codable is quite easy, as following
class LoginModal: Coadable
{
let cashierType: NSNumber
let status: NSNumber
}
as mentioned above keys may missing, but we don't want the Model Variables as optional, So How we can achieve this with Codable.
Thanks
Use init(from decoder: Decoder) to set the default values in your model.
struct LoginModal: Codable {
let cashierType: Int
let status: Int
enum CodingKeys: String, CodingKey {
case cashierType = "cashierType"
case status = "status"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.cashierType = try container.decodeIfPresent(Int.self, forKey: .cashierType) ?? 0
self.status = try container.decodeIfPresent(Int.self, forKey: .status) ?? 0
}
}
Data Reading:
do {
let data = //JSON Data from API
let jsonData = try JSONDecoder().decode(LoginModal.self, from: data)
print("\(jsonData.status) \(jsonData.cashierType)")
} catch let error {
print(error.localizedDescription)
}

How to use init method in codable when you have model inside?

New in this code world and thanks in advance,
I am getting error
Cannot assign value of type 'String?' to type 'ModalA.ModalC?'
Here is my model class,
struct ModalA: Codable {
struct ModalB: Codable {
let value2: String?
let value3: ModalC?
private enum CodingKeys: String, CodingKey {
case value3 = "Any"
case value2 = "Anything"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
value2 = try values.decodeIfPresent(String.self, forKey: .value2)
value3 = try values.decodeIfPresent(String.self, forKey: .value3) // getting error on this line
}
}
struct ModalC: Codable {
let value3: String?
}
let value1: ModalB?
}
How to solve this error?
Your value3 property is of type ModalC, but when decoding you are trying to parse String value (when passing String.self to decodeIfPresent method).
decodeIfPresent method takes type of decodable value as first argument. In your case decodeIfPresent method returns String value and you are trying to set String value to property of ModalC type.
So to solve the error you should say that you want to get value of type ModalC for key .value3. To do this you should pass ModalC.self like so:
value3 = try values.decodeIfPresent(ModalC.self, forKey: .value3)
You can fix this by
value3 = try values.decodeIfPresent(ModalC.self, forKey: .value3)
but declaring value3 as optional
let value3: ModalC?
will fetch it if it's originally exists in the parsed json so ? is enough
you should use
init(){
}
init(from decoder: Decoder) throws{
}
you can read my post here for mor info.

Resources