Swift Codable null handling - ios

I have a struct that parse JSON using Codable.
struct Student: Codable {
let name: String?
let amount: Double?
let adress: String?
}
Now if the amount value is coming as null the JSON parsing is failing.
So should I manually handle the null cases for all the Int and Double that are present in the Student struct?
The String values coming as null is automatically handled.

Let me do this Playground for you since an example shows you more than a hundred words:
import Cocoa
struct Student: Codable {
let name: String?
let amount: Double?
let adress: String?
}
let okData = """
{
"name": "here",
"amount": 100.0,
"adress": "woodpecker avenue 1"
}
""".data(using: .utf8)!
let decoder = JSONDecoder()
let okStudent = try decoder.decode(Student.self, from:okData)
print(okStudent)
let nullData = """
{
"name": "there",
"amount": null,
"adress": "grassland 2"
}
""".data(using: .utf8)!
let nullStudent = try decoder.decode(Student.self, from:nullData)
print(nullStudent)
null is handled just fine if you define your structs using optionals. I would however advise against it if you can avoid it. Swift provides the best support I know to help me not forgetting to handle nil cases wherever they may occur, but they are still a pain in the ass.

Was browsing through Codable and got this issue.
So to be very clear here it is,
If the JSON/response would contain null as the value, its interpreted as nil. And hence for that reason one of the model's property which might contain null should be marked as optional.
For Example, consider the below JSON response,
{
"name": "Steve",
"amount": null,
"address": "India"
}
The model should be as below, cos amount is returning null.
struct Student: Codable {
let name: String
let amount: Double?
let address: String
}
Suggestion: In case, if you would write a init(from decoder: Decoder) throws, always use something like below, for optional properties.
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
amount = try values.decodeIfPresent(String.self, forKey: .amount)
//so on...
}
Even if you add do-catch block with try? decoder.... it can be captured if fails. Hope thats clear!! Its simple but very difficult to find the issue even if the model is containing 5 or more properties with some containing null values

Related

Convert quoted number from JSON (swift)

I have a struct like bellow:
struct MyStruct: Codable {
var id: Int?
}
and the JSON that i was receiving from server was like this:
{
"id": 12345
}
But now server-side decided to send all numbers as quoted numbers like this:
{
"id": "12345"
}
When decoding this json using JSONDecoder().decode i got an error,
The data couldn’t be read because it isn’t in the correct format
Is there any way, (except writing custom Encodable and Decodable implementation for every struct that i created up to now) to solve this problem? For example doing something on JSONDecoder()
You can do it by implementing Decodable protocol's required initializer, init(from:):
extension MyStruct {
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let idString = try values.decode(String.self, forKey: .id)
id = Int(idString)
}
}
And don't forget to decode values of other properties.

Swift Decodable parse part of JSON

I'm trying to parse a json that looks like this using decodable:
{
"count": 1,
"results": [
{
"title": 1,
"content": "Bla"
} ]
}
My problem is that I don't want to make a class that has a count property just to be able to use the decoder. I want to parse only the results part I don't care about the count.
So my question is, can decodable.decode somehow only parse a part of the result json. I mean a certain key path instead of the whole json ? And I want to do it using Decodable.
In a nutshell I don't want this:
class IncidentWrapper: Codable{
var count: Int
var incident: [Incident]
}
What I would Imagine is to have this:
decodable.decode([Incident].self, from: response.data, forKey: "results")
Thanks
let me see what I can suggest:
struct Result: Codeable {
var id: Int
var message: String
var color: String
var type: String
enum CodingKeys: String, CodingKey {
case results
}
enum NestedResultKeys: String, CodingKey {
case id, message, color, type
}
}
extension Result: Decodable {
init(from decoder: Decoder) throws {
let result = try decoder.container(keyedBy: CodingKeys.self)
let nestedResult = try result.nestedContainer(keyedBy: NestedResultKeys.self, forKey: .result)
id = try nestedResult.decode(Int.self, forKey: .id)
message = try nestedResult.decode(String.self, forKey: .message)
color = try nestedResult.decode(String.self, forKey: .color)
type = try nestedResult.decode(String.self, forKey: .id)
}
}
See this documentation for more insight
https://developer.apple.com/documentation/swift/swift_standard_library/encoding_decoding_and_serialization
Hope it helps your project!
You probably is looking for JSONSerialization class. This is an example how it works:
if let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String : Any] {
if let results = json["results"] as? [Incident] {
print(results.count)
}
}
You can define a generic wrapper for once and use everywhere.
It will work as a generic wrapper for results key only.
protocol ResultsDecodable: Decodable{
associatedtype T: Decodable
var results: [T] {get}
}
struct Result<Element: Decodable>: ResultsDecodable{
typealias T = Element
var results: [Element]
}
Extend JSONDecoder to get results output.
extension JSONDecoder {
func resultDecode<T>(_ type: Result<T>.Type, from data: Data) throws -> [T] where T : Decodable{
let model = try! decode(type, from: data)
return model.results
}
}
And use like this
var str = #"{"count": 1,"results": [{"title": 1,"content": "Bla"}, {"title": 2,"content": "Bla"} ]}"#
class Incident: Decodable{
let title: Int
let content: String
}
let indicents = (try! JSONDecoder().resultDecode(Result<Incident>.self, from: str.data(using: .utf8)!))
See how it makes everything more complex. BETTER USE IncidentWrapper!!!
You only need to use the keys you care about. Just leave off the count. Don't make it part of your struct.
You will only get errors if you can't find a key in the json that you are expecting in the struct. You can avoid this too, though, if you make it an optional in the struct.

How do I properly decode this json string using decodable?

I have the following json string:
{"weight":[{"bmi":24.75,"date":"2020-01-20","logId":1000,"source":"API","time":"23:59:59","weight":200}]}
I want to convert it to a Swift object in order to access the different values. Here is what I am trying to do, I have these structs setup:
struct FitbitResponseModel: Decodable {
let weight: [FitbitResponseData]
}
struct FitbitResponseData: Decodable {
let bmi: Int
let date: String
let logId: Int
let source: String
let time: String
let weight: Int
}
And then I have this method to decode the json string:
func parseJSON(data: Data) -> FitbitResponseModel? {
var returnValue: FitbitResponseModel?
do {
returnValue = try JSONDecoder().decode(FitbitResponseModel.self, from: data)
} catch {
print("Error took place: \(error.localizedDescription).")
}
return returnValue
}
However when I try to run it I get the error that the data couldn’t be read because it isn’t in the correct format. What am I doing wrong? Any help is appreciated.
Thanks in advance!
change
let bmi: Int
to
let bmi: Double
beacuse it's value is coming out to be 24.75 in your response if any variable type doesn't match to JSON response whole model wouldn't map in Codable protocol (Encodable and Decodable)
Talk to your API developer. 000 is not a valid representation of a number for json. It needs to be either 0 or 0.0. You can lint your json at https://jsonlint.com . If you really need to work around this I suggest doing a string replacement on 000, with 0, before you parse the data.
Json is n't valid because logId value in your json is n't valid.
{
"weight": [{
"bmi": 24.75,
"date": "2020-01-20",
"logId": 100,
"source": "API",
"time": "23:59:59",
"weight": 200
}]
}
One really neat feature of this auto-generated conformance is that if you define an enum in your type called "CodingKeys" (or use a type alias with this name) that conforms to the CodingKey protocol – Swift will automatically use this as the key type. This therefore allows you to easily customise the keys that your properties are encoded/decoded with.
struct Base: Codable {
let weight : [Weight]?
enum CodingKeys: String, CodingKey {
case weight = "weight"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
weight = try values.decodeIfPresent([Weight].self, forKey: .weight)
}
}
struct Weight : Codable {
let bmi : Double?
let date : String?
let logId : Int?
let source : String?
let time : String?
let weight : Int?
enum CodingKeys: String, CodingKey {
case bmi = "bmi"
case date = "date"
case logId = "logId"
case source = "source"
case time = "time"
case weight = "weight"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
bmi = try values.decodeIfPresent(Double.self, forKey: .bmi)
date = try values.decodeIfPresent(String.self, forKey: .date)
logId = try values.decodeIfPresent(Int.self, forKey: .logId)
source = try values.decodeIfPresent(String.self, forKey: .source)
time = try values.decodeIfPresent(String.self, forKey: .time)
weight = try values.decodeIfPresent(Int.self, forKey: .weight)
}
}
Hope that will help!
or you can use SwiftyJSON lib: https://github.com/SwiftyJSON/SwiftyJSON

How to make Swift Codable types more versatile

I currently am working with an API that deals with bus predictions. There is an interesting quirk with the JSON that is returned for the predictions of a certain stop. When there are multiple predictions for a stop, the JSON looks something like this:
...
"direction": {
"prediction": [
{
"affectedByLayover": "true",
"block": "241",
"dirTag": "loop",
"epochTime": "1571785998536",
"isDeparture": "false",
"minutes": "20",
"seconds": "1208",
"tripTag": "121",
"vehicle": "1698"
},
{
"affectedByLayover": "true",
"block": "241",
"dirTag": "loop",
"epochTime": "1571787798536",
"isDeparture": "false",
"minutes": "50",
"seconds": "3008",
"tripTag": "122",
"vehicle": "1698"
},
{
"affectedByLayover": "true",
"block": "241",
"dirTag": "loop",
"epochTime": "1571789598536",
"isDeparture": "false",
"minutes": "80",
"seconds": "4808",
"tripTag": "123",
"vehicle": "1698"
}
],
"title": "Loop"
}
...
However, when there is only one prediction for a stop, the JSON looks like this instead:
...
"direction": {
"prediction":
{
"affectedByLayover": "true",
"block": "241",
"dirTag": "loop",
"epochTime": "1571785998536",
"isDeparture": "false",
"minutes": "20",
"seconds": "1208",
"tripTag": "121",
"vehicle": "1698"
}
"title": "Loop"
}
...
Notice that the "prediction" is no longer inside an array -- this is where I believe things are getting complicated when using a Swift Codable type to decode the JSON. My model looks like this for the "direction" and "prediction"
struct BTDirection: Codable {
let title: String!
let stopTitle: String!
let prediction: [BTPrediction]!
}
struct BTPrediction: Codable {
let minutes: String!
let vehicle: String!
}
Basically what is happening is prediction in BTDirection is looking for an Array of BTPrediction, however in the 2nd case above, this wouldn't be an Array so the decoding fails. How can I make my models more flexible to accommodate both an array or a single object? Ideally, in the 2nd case prediction would still be an array of a single BTDirection. Any help on this would be much appreciated.
You can try
struct BTDirection:Codable {
let title,stopTitle: String
let prediction: [BTPrediction]
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
title = try container.decode(String.self, forKey: .title)
stopTitle = try container.decode(String.self, forKey: .stopTitle)
do {
let res = try container.decode([BTPrediction].self, forKey: .prediction)
prediction = res
}
catch {
let res = try container.decode(BTPrediction.self, forKey: .prediction)
prediction = [res]
}
}
}
To add on to Sh_Khan's answer, if you have multiple places in your API responses where this sort of thing happens, you can extract this custom decoding and encoding out to a custom wrapper type so you don't have to repeat it everywhere, like:
/// Wrapper type that can be encoded/decoded to/from either
/// an array of `Element`s or a single `Element`.
struct ArrayOrSingleItem<Element> {
private var elements: [Element]
}
extension ArrayOrSingleItem: Decodable where Element: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
// First try decoding the single value as an array of `Element`s.
elements = try container.decode([Element].self)
} catch {
// If decoding as an array of `Element`s didn't work, try decoding
// the single value as a single `Element`, and store it in an array.
elements = try [container.decode(Element.self)]
}
}
}
extension ArrayOrSingleItem: Encodable where Element: Encodable {
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
if elements.count == 1, let element = elements.first {
// If the wrapped array of `Element`s has exactly one `Element`
// in it, encode this as just that one `Element`.
try container.encode(element)
} else {
// Otherwise, encode the wrapped array just as it is - an array
// of `Element`s.
try container.encode(elements)
}
}
}
// This lets you treat an `ArrayOrSingleItem` like a collection of elements.
// If you need the elements as type `Array<Element>`, just instantiate a new
// `Array` from your `ArrayOrSingleItem` like:
// let directions: ArrayOrSingleItem<BTDirection> = ...
// let array: [BTDirection] = Array(directions)
extension ArrayOrSingleItem: MutableCollection {
subscript(position: Int) -> Element {
get { elements[position] }
set { elements[position] = newValue }
}
var startIndex: Int { elements.startIndex }
var endIndex: Int { elements.endIndex }
func index(after i: Int) -> Int {
elements.index(after: i)
}
}
// This lets you instantiate an `ArrayOrSingleItem` from an `Array` literal.
extension ArrayOrSingleItem: ExpressibleByArrayLiteral {
init(arrayLiteral elements: Element...) {
self.elements = elements
}
}
Then you can just declare your prediction (and any other property that has the potential to be either an array or a single item in your API response) like this:
struct BTDirection: Codable {
let title: String?
let stopTitle: String?
let prediction: ArrayOrSingleItem<BTPrediction>?
}
Both #TylerTheCompiler & #Sh_Khan provide very good technical input into solution that provide the mechanics of a solution, but the provided code will hit some implementation issues with the given json data:
there are errors in the JSON posted that will stop codable working with it - I suspect these are just copy & paste errors, but if not you will have issues moving forwards.
Because of the initial direction key the JSON effectively has 3 (or at least 2.5!) layers of nesting. This will either need to be flattened in init(from:) or, as in the below, need a temporary struct for ease of mapping. Flattening in the initialiser would be more elegant, a temporary struct is far quicker :-)
CodingKeys, while obvious, isn't defined in the previous answers, so will cause errors compiling the init(from:)
there's no stopTitle field in the JSON, so that will error on decoding unless it is treated as an optional. Here I've treated it as a concrete String and handled it in the decoding; you could just make it a String? and then the decoder would cope with it being absent.
Using "corrected" JSON (added opening braces, missing commas, etc) the following code will import both scenarios. I've not implemented the arrayOrSingleItem as all credit for that belongs with #TylerTheCompiler, but you could easily drop it in.
struct Direction: Decodable {
let direction: BTDirection
}
struct BTDirection: Decodable {
enum CodingKeys: String, CodingKey {
case title
case stopTitle
case prediction
}
let prediction: [BTPrediction]
let title: String
let stopTitle: String
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
prediction = try container.decode([BTPrediction].self, forKey: .prediction)
} catch {
let singlePrediction = try container.decode(BTPrediction.self, forKey: .prediction)
prediction = [singlePrediction]
}
title = try container.decode(String.self, forKey: .title)
stopTitle = try container.decodeIfPresent(String.self, forKey: .stopTitle) ?? "unnamed stop"
}
}
struct BTPrediction: Decodable {
let minutes: String
let vehicle: String
}
and then to actually decode the JSON decode the top-level Direction type
let data = json.data(using: .utf8)
if let data = data {
do {
let bus = try decoder.decode(Direction.self, from: data)
// extract the BTDirection type from the temporary Direction type
// and do something with the decoded data
}catch {
//handle error
}
}
In case you're not aware, JSON Validator is very useful for validating/correcting json.

How to get the nondecoded attributes from a Decoder container in Swift 4?

I'm using the Decodable protocol in order to parse JSON received from an external source. After decoding the attributes that I do know about there still may be some attributes in the JSON that are unknown and have not yet been decoded. For example, if the external source added a new attribute to the JSON at some future point in time I would like to hold onto these unknown attributes by storing them in a [String: Any] dictionary (or an alternative) so the values do not get ignored.
The issue is that after decoding the attributes that I do know about there isn't any accessors on the container to retrieve the attributes that have not yet been decoded. I'm aware of the decoder.unkeyedContainer() which I could use to iterate over each value however this would not work in my case because in order for that to work you need to know what value type you're iterating over but the value types in the JSON are not always identical.
Here is an example in playground for what I'm trying to achieve:
// Playground
import Foundation
let jsonData = """
{
"name": "Foo",
"age": 21
}
""".data(using: .utf8)!
struct Person: Decodable {
enum CodingKeys: CodingKey {
case name
}
let name: String
let unknownAttributes: [String: Any]
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
// I would like to store the `age` attribute in this dictionary
// but it would not be known at the time this code was written.
self.unknownAttributes = [:]
}
}
let decoder = JSONDecoder()
let person = try! decoder.decode(Person.self, from: jsonData)
// The `person.unknownAttributes` dictionary should
// contain the "age" attribute with a value of 21.
I would like for the unknownAttributes dictionary to store the age attribute and value in this case and any other possible value types if they get added to the JSON from the external source in the future.
The reason I am wanting to do something like this is so that I can persist the unknown attributes present in the JSON so that in a future update of the code I will be able to handle them appropriately once the attribute keys are known.
I've done plenty of searching on StackOverflow and Google but haven't yet encountered this unique case. Thanks in advance!
You guys keep coming up with novel ways to stress the Swift 4 coding APIs... ;)
A general solution, supporting all value types, might not be possible. But, for primitive types, you can try this:
Create a simple CodingKey type with string-based keys:
struct UnknownCodingKey: CodingKey {
init?(stringValue: String) { self.stringValue = stringValue }
let stringValue: String
init?(intValue: Int) { return nil }
var intValue: Int? { return nil }
}
Then write a general decoding function using the standard KeyedDecodingContainer keyed by the UnknownCodingKey above:
func decodeUnknownKeys(from decoder: Decoder, with knownKeys: Set<String>) throws -> [String: Any] {
let container = try decoder.container(keyedBy: UnknownCodingKey.self)
var unknownKeyValues = [String: Any]()
for key in container.allKeys {
guard !knownKeys.contains(key.stringValue) else { continue }
func decodeUnknownValue<T: Decodable>(_ type: T.Type) -> Bool {
guard let value = try? container.decode(type, forKey: key) else {
return false
}
unknownKeyValues[key.stringValue] = value
return true
}
if decodeUnknownValue(String.self) { continue }
if decodeUnknownValue(Int.self) { continue }
if decodeUnknownValue(Double.self) { continue }
// ...
}
return unknownKeyValues
}
Finally, use the decodeUnknownKeys function to fill your unknownAttributes dictionary:
struct Person: Decodable {
enum CodingKeys: CodingKey {
case name
}
let name: String
let unknownAttributes: [String: Any]
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
let knownKeys = Set(container.allKeys.map { $0.stringValue })
self.unknownAttributes = try decodeUnknownKeys(from: decoder, with: knownKeys)
}
}
A simple test:
let jsonData = """
{
"name": "Foo",
"age": 21,
"token": "ABC",
"rate": 1.234
}
""".data(using: .utf8)!
let decoder = JSONDecoder()
let person = try! decoder.decode(Person.self, from: jsonData)
print(person.name)
print(person.unknownAttributes)
prints:
Foo
["age": 21, "token": "ABC", "rate": 1.234]

Resources