Handle string and int received in json [duplicate] - ios

This question already has answers here:
Swift 4 Codable - Bool or String values
(1 answer)
Swift 4 Codable - API provides sometimes an Int sometimes a String
(1 answer)
Closed 2 years ago.
I have a json which looks like so...
{
id = 123456;
isDeleted = 0;
name = testName;
parentId = "<null>"; // This can also be integer
plantId = 1223; // This can also be string
type = 1;
}
In the response above, I can get either a string or an int for both parentId & plantId. How can I handle both the cases..?
This is how my structure looks...
struct Root : Decodable {
let organizations : [Organization1]
}
struct Organization1 : Decodable {
let id: Int
let isDeleted: Bool
let name: String
let parentId: Int?
let type: Int
let plantId: String?
let loggedInUserId: Int?
}

You can do that like this
struct GeneralProduct: Decodable {
let id: Int
let isDeleted: Bool
let name: String
let parentId: String?
let type: Int
let plantId: String?
let loggedInUserId: Int?
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
isDeleted = try container.decode(Bool.self, forKey: .isDeleted)
name = try container.decode(String.self, forKey: .id)
type = try container.decode(Int.self, forKey: .type)
loggedInUserId = try container.decode(Int.self, forKey: .loggedInUserId)
if let value = try? container.decode(Int.self, forKey: .parentId) {
parentId = String(value)
} else {
parentId = try container.decode(String.self, forKey: .id)
}
if let value = try? container.decode(Int.self, forKey: .plantId) {
plantId = String(value)
} else {
plantId = try container.decode(String.self, forKey: .id)
}
}
}

Related

Decoding fails if keys not present

Decoding fails if keys not present. How to safely decode if missing keys also.
I have gone through that use Optional or Nil values but still not able to decoding the objects.
Below my Json
{
"mandatory":true,
"dynamic_obj":[
{
"dt":"2021-09-22 01:29:52",
"url":"https://res._22_01_29.pdf",
"desc":"PAN CARD",
"flag":1,
"count":"2",
"field":"pan_card",
"address":"300-435, Nattu Muthu St, Sheethammal Colony, Venus Colony, Chennai, Tamil Nadu 600018, India",
"visible":true,
"latitude":13.0389309,
"longitude":80.2473746
},
{
"url":"https://res.cloudin/no-image.jpg",
"desc":"driving License",
"count":"2",
"field":"driving_license",
"visible":true
}
]
}
Model class below
struct Dynamic_obj : Codable {
var dt : String?
var url : String?
let desc : String?
var flag : Int?
let count : String?
let field : String?
let visible : Bool?
var bankname : String = "NA"
var pdfPassword : String = "NA"
var latitude : String = "NA"
var longitude : String = "NA"
var address : String = "NA"
enum CodingKeys: String, CodingKey {
case dt = "dt"
case url = "url"
case desc = "desc"
case flag = "flag"
case count = "count"
case field = "field"
case visible = "visible"
case bankname = "bankname"
case pdfPassword = "pdfPassword"
case latitude = "latitude"
case longitude = "longitude"
case address = "address"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
dt = try values.decodeIfPresent(String.self, forKey: .dt)
url = try values.decodeIfPresent(String.self, forKey: .url)
desc = try values.decodeIfPresent(String.self, forKey: .desc)
flag = try values.decodeIfPresent(Int.self, forKey: .flag)
count = try values.decodeIfPresent(String.self, forKey: .count)
field = try values.decodeIfPresent(String.self, forKey: .field)
visible = try values.decodeIfPresent(Bool.self, forKey: .visible)
bankname = try values.decodeIfPresent(String.self, forKey: .bankname) ?? "NA"
pdfPassword = try values.decodeIfPresent(String.self, forKey: .pdfPassword) ?? "NA"
latitude = try values.decodeIfPresent(String.self, forKey: .latitude) ?? "NA"
longitude = try values.decodeIfPresent(String.self, forKey: .longitude) ?? "NA"
address = try values.decodeIfPresent(String.self, forKey: .address) ?? "NA"
}
}
let decoder = JSONDecoder()
do {
let responseModel = try decoder.decode(LoanDocxPending.self, from: data)
if let mandotory = responseModel.mandatory{
self.visibleDocuments["Identity Proof-~id_proof"] = idProof
self.visibleTitle.append("Identity Proof")
}
} catch {
print("error")
}
struct LoanDocxPending :Codable {
let mandatory : Bool?
var dynamic_obj : [Dynamic_obj]?
enum CodingKeys: String, CodingKey {
case mandatory = "mandatory"
case dynamic_obj = "dynamic_obj"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
mandatory = try values.decodeIfPresent(Bool.self, forKey: .mandatory)
dynamic_obj = try values.decodeIfPresent([Dynamic_obj].self, forKey: .dynamic_obj)
}
}
First of all most of your code is not needed, for example the init methods and almost all CodingKeys.
Second of all as mentioned in the comments rather than printing meaningless literal "error" print the error instance. It will tell you that latitude and longitude are Double, not String.
This model matches the JSON in the question
struct DynamicObj : Codable {
let dt : String?
let url, desc : String
let flag : Int?
let count, field : String
let visible : Bool
var bankname, pdfPassword, address : String?
var latitude, longitude : Double?
}
struct LoanDocxPending :Codable {
let mandatory : Bool
var dynamicObj : [DynamicObj]
enum CodingKeys: String, CodingKey {
case mandatory,dynamicObj = "dynamic_obj"
}
}
let decoder = JSONDecoder()
do {
let responseModel = try decoder.decode(LoanDocxPending.self, from: data)
print(responseModel.mandatory)
} catch {
print("error", error)
}

Can I use both init() and initial value in Decoder?

I have lots of values in my codable struct. I have URLs coming in as ""(empty string) so I need custom Decoder to convert "" as nil. So I made a propertyWrapper to solve this.
For example, I have values like ["https://google.com", "", "https://google.com"] and I want to make it as [URL?]. This works well with my decoder. It's converted as [URL("https://google.com"), nil, URL("https://google.com")]
However, I found a problem that, when I use init(from decoder: Decoder) throws, I also have to initialize all other values in struct. Is there any way to use just courseImages and use other values in struct as set?
struct CabinetCourse: Codable {
let courseId: String
let title: String
let planStartDate: Date
let planEndDate: Date
let companionTypeCd: String
let courseCategory: String
let planId: String
let nickname: String?
let isCabinet: Bool
let isFavorite: Bool?
let course: String
let score: String
let shareCnt: Int
let favoriteCnt: Int
let cabinetCnt: Int
let placeCount: Int
let createDt: Date
let childPlaceCount: Int
let wheelChairPlaceCount: Int
let elderPlaceCount: Int
#OptionalObject
var courseImages: [URL?]
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let emptyURLS = try values.decode([OptionalObject<URL>].self, forKey: .courseImages)
courseImages = emptyURLS.map { $0.wrappedValue }
// => these are the lines I don't want to write
courseId = try values.decode(String.self, forKey: .courseId)
title = try values.decode(String.self, forKey: .title)
planStartDate = try values.decode(Date.self, forKey: .planStartDate)
planEndDate = try values.decode(Date.self, forKey: .planEndDate)
companionTypeCd = try values.decode(String.self, forKey: .companionTypeCd)
courseCategory = try values.decode(String.self, forKey: .courseCategory)
planId = try values.decode(String.self, forKey: .planId)
nickname = try values.decode(String.self, forKey: .nickname)
isCabinet = try values.decode(Bool.self, forKey: .isCabinet)
isFavorite = try values.decode(Bool.self, forKey: .isCabinet)
course = try values.decode(String.self, forKey: .isCabinet)
score = try values.decode(String.self, forKey: .isCabinet)
}
#propertyWrapper
struct OptionalObject<Base: Decodable>: Decodable {
var wrappedValue: Base?
init(from decoder: Decoder) throws {
do {
let container = try decoder.singleValueContainer()
wrappedValue = try container.decode(Base.self)
} catch {
wrappedValue = nil
}
}
}
You are misunderstanding property wrappers. They "decorate" the entire property type - [URL?], not just the array element type URL?. [URL?] doesn't match the type of wrappedValue. [URL]? does (Base == [URL]), but that's not what you want.
One way to create a property wrapper that can be applied to an array of optionals is:
#propertyWrapper
struct OptionalArray<Base: Decodable>: Decodable {
var wrappedValue: [Base?]
...
Now Base == URL matches [URL?], and in init, you have to decode a [Base?]:
init(from decoder: Decoder) throws {
var arr = [Base?]()
var container = try decoder.unkeyedContainer()
for _ in 0..<(container.count ?? 0) {
if let element = try? container.decode(Base.self) {
arr.append(element)
} else {
arr.append(nil)
_ = try container.decode(String.self) // advances the decoder to the next position
}
}
wrappedValue = arr
}
Once you have the property wrapper, you don't need the custom decoding code at all. Swift figures it out.
struct CabinetCourse: Decodable {
let courseId: String
let title: String
let planStartDate: Date
let planEndDate: Date
let companionTypeCd: String
let courseCategory: String
let planId: String
let nickname: String?
let isCabinet: Bool
let isFavorite: Bool?
let course: String
let score: String
let shareCnt: Int
let favoriteCnt: Int
let cabinetCnt: Int
let placeCount: Int
let createDt: Date
let childPlaceCount: Int
let wheelChairPlaceCount: Int
let elderPlaceCount: Int
#OptionalArray
var courseImages: [URL?]
}
// That's it!
For the encoding part, it depends on how you want to encode the nils. But either way, the code is very similar to the decoding code.

How to decode DynamicKeys & CodingKeys in the same container?

Consider the following JSON: I'm trying to decode the "teams" object.
let jsonString = """
{
"Superheroes":{
"Marvel":"107",
"DC":"106"
},
"teams":{
"106":{
"name":"Marvel",
"Superheroes":{
"890":{
"name":"Batman"
}
}
},
"107":{
"name":"DC",
"Superheroes":{
"891":{
"name":"Wonder Woman"
}
}
}
}
}
"""
I have tried something like this:
struct SuperheroResponse: Decodable {
let teams: [Team]
private enum CodingKeys: String, CodingKey {
case teams = "teams"
}
private struct DynamicCodingKeys: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int?
init?(intValue: Int) {
return nil
}
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let teamContainer = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.teams)
print(teamContainer.allKeys.count)
let tempArray: [Team] = []
for key in teamContainer.allKeys {
let decodedObject = try teamContainer.decode(Team.self, forKey: DynamicCodingKeys(stringValue: key.stringValue)!)
tempArray.append(decodedObject)
}
teams = tempArray
}
}
struct Team: Decodable {
let name: String
}
I thought that first I would get the teams container, map over the keys and go on from there. Problem is teamContainer.allKeys.count is always zero.
Also the following line, results in following error: Cannot convert value of type 'SuperheroResponse.DynamicCodingKeys' to expected argument type 'SuperheroResponse.CodingKeys'
let decodedObject = try teamContainer.decode(Team.self, forKey: DynamicCodingKeys(stringValue: key.stringValue)!)
Finally I decode it as follows:
let jsonData = Data(jsonString.utf8)
let decodedResult = try! JSONDecoder().decode(SuperheroResponse.self, from: jsonData)
dump(decodedResult)
Any help would be appreciated. Ideally I would like something like SuperheroResponse -> [Team],
Team -> name, [Superhero], Superhero -> name
You just have a couple of minor mistakes. You're almost there.
The team container is keyed by DynamicCodingKeys:
let teamContainer = try container.nestedContainer(keyedBy: DynamicCodingKeys.self, // <=
forKey: .teams)
And the Teams can be decoded as using the key you're given:
let decodedObject = try teamContainer.decode(Team.self, forKey: key)
Also, tempArray needs to be var:
var tempArray: [Team] = []
Or replace that loop with a map:
teams = try teamContainer.allKeys.map {
try teamContainer.decode(Team.self, forKey: $0)
}
All together:
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let teamContainer = try container.nestedContainer(keyedBy: DynamicCodingKeys.self, forKey: .teams)
teams = try teamContainer.allKeys.map {
try teamContainer.decode(Team.self, forKey: $0)
}
}

Change variable of all elements inside an array if Set contains it

I know this is a silly question but I don't know which will be the more performant solution. I have an array of ListItem. ListItem has a boolean value isSelected. I also have Set<ListItem>. I want to change that boolean to true if array has an element which is also inside Set. How can I achieve this with the best performance?
My Set and Array:
var selectedItems = Set<ListItem>()
var array: [ListItem] = []
List Item:
class ListItem: Codable ,Equatable, Hashable {
let wrapperType: String
let kind: String?
let trackId: Int?
let artistId: Int?
let collectionId: Int?
let artistName, collectionName, trackName: String?
let trackViewUrl: String?
let artworkUrl30, artworkUrl60,artworkUrl100: String?
let releaseDate: String?
let primaryGenreName: String?
var isSelected: Bool = false
enum CodingKeys: String, CodingKey {
case wrapperType, kind
case artistId
case collectionId
case trackId
case artistName, collectionName, trackName
case trackViewUrl
case artworkUrl30, artworkUrl60, artworkUrl100, releaseDate, primaryGenreName
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
wrapperType = try container.decode(String.self, forKey: .wrapperType)
print(wrapperType)
kind = try container.decodeIfPresent(String.self, forKey: .kind)
trackId = try container.decodeIfPresent(Int.self, forKey: .trackId)
collectionId = try container.decodeIfPresent(Int.self, forKey: .collectionId)
artistId = try container.decodeIfPresent(Int.self, forKey: .artistId)
artistName = try container.decodeIfPresent(String.self, forKey: .artistName)
collectionName = try container.decodeIfPresent(String.self, forKey: .collectionName)
trackName = try container.decodeIfPresent(String.self, forKey: .trackName)
trackViewUrl = try container.decodeIfPresent(String.self, forKey: .trackViewUrl)
artworkUrl30 = try container.decodeIfPresent(String.self, forKey: .artworkUrl30)
artworkUrl100 = try container.decodeIfPresent(String.self, forKey: .artworkUrl100)
artworkUrl60 = try container.decodeIfPresent(String.self, forKey: .artworkUrl60)
releaseDate = try container.decodeIfPresent(String.self, forKey: .releaseDate)
primaryGenreName = try container.decodeIfPresent(String.self, forKey: .primaryGenreName)
}
static func ==(lhs: ListItem, rhs: ListItem) -> Bool {
return lhs.trackName == rhs.trackName
}
func hash(into hasher: inout Hasher) {
if trackId != nil {
hasher.combine(trackName)
} else if collectionId != nil {
//AudioBooks Doesn't have TrackId
hasher.combine(collectionId)
} else {
print("Both TrackId && Collection Id is null")
}
}
}
Because these are reference types, if it is guaranteed that there is only one instance of any unique ListItem, it would be sufficient to just set isSelected to false for each item in your array, and then set isSelected to true for each item in your selectedItems.
array.forEach { $0.isSelected = false }
selectedItems.forEach { $0.isSelected = true }
If there can be more than one instance of an item, you are going to have to iterate the items in your array and check if the Set contains them. Fortunately, contains is O(1) for a Set:
array.forEach { $0.isSelected = selectedItems.contains($0) }
Note: It is vital that the hashValue be equal for ListItems that are equal, or this all breaks down. Your hash(into:) function is currently using more fields than your == function, so it is possible to generate a different hashValue for equal ListItems. Fix this to ensure that a proper hashValue is generated.

How to do custom transformations using Codable?

Let's say I need to transform a date string I received from a web service to a Date object.
Using ObjectMapper, that was easy:
class Example: Mappable {
var date: Date?
required init?(map: Map) { }
func mapping(map: Map) {
date <- (map["date_of_interest"], GenericTransform().dateTransform)
}
}
I just had to implement a tranformer ("GenericTransform" in this case) for date, and pass it as an argument along with the key name to decode.
Now, using Codable:
class Example2: Codable {
var name: String
var age: Int
var date: Date?
enum CodingKeys: String, CodingKey {
case name, age
case date = "date_of_interest"
}
}
To transform a date, in my understanding, I'd have to either:
1) Pass a dateDecodingStrategy to my JSONDecoder, which I don't want to, because I'm trying to keep that part of the code as a generic function.
or
2) Implement an init(from decoder: Decoder) inside Example2, which I also don't want to, because of the boilerplate code I'd have to write to decode all the other properties (which would be automatically generated otherwise):
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
age = try container.decode(Int.self, forKey: .age)
let dateString = try container.decode(String.self, forKey: .date)
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
if let date = formatter.date(from: dateString) {
self.date = date
} else {
//throw error
}
}
My question is: is there an easier way to do it than options 1 and 2 above?
Maybe tweaking the CodingKeys enum somehow?
EDIT:
The problem is not only about dates, actually. In this project that I'm working on, there are many custom transformations being done using TransformOf<ObjectType, JSONType> from ObjectMapper.
For example, a color transformation of a hex code received from a web service into a UIColor is done using this bit of code:
let colorTransform = TransformOf<UIColor, String>(fromJSON: { (value) -> UIColor? in
if let value = value {
return UIColor().hexStringToUIColor(hex: value)
}
return nil
}, toJSON: { _ in
return nil
})
I'm trying to remove ObjectMapper from the project, making these same transformations using Codable, so only using a custom dateDecodingStrategy will not suffice.
How would you guys do it? Implement a custom init(from decoder: Decoder) for every class that has to decode, for example, a color hex code?
Using dateDecodingStrategy in your case (as you only reference a single date format) is very simpleā€¦
let decoder = JSONDecoder()
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
decoder.dateDecodingStrategy = .formatted(formatter)
We can use custom method for example like decodeAll here. Try in playground.
struct Model: Codable {
var age: Int?
var name: String?
enum CodingKeys: String, CodingKey {
case name
case age
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decodeAll(String.self, forKey: .name)
age = try container.decodeAll(Int.self, forKey: .age)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try? container.encode(name, forKey: .name)
try? container.encode(age, forKey: .age)
}
}
extension KeyedDecodingContainer where K: CodingKey, K: CustomDebugStringConvertible {
func decodeAll<T: Decodable>(_ type: T.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> T {
if let obj = try? decode(T.self, forKey: key) {
return obj
} else {
if type == String.self {
if let obj = try? decode(Int.self, forKey: key), let val = String(obj) as? T {
return val
} else if let obj = try? decode(Double.self, forKey: key), let val = String(obj) as? T {
return val
}
} else if type == Int.self {
if let obj = try? decode(String.self, forKey: key), let val = Int(obj) as? T {
return val
} else if let obj = try? decode(Double.self, forKey: key), let val = Int(obj) as? T {
return val
}
} else if type == Double.self {
if let obj = try? decode(String.self, forKey: key), let val = Double(obj) as? T {
return val
} else if let obj = try? decode(Int.self, forKey: key), let val = Double(obj) as? T {
return val
}
}
}
throw DecodingError.typeMismatch(T.self, DecodingError.Context(codingPath: codingPath, debugDescription: "Wrong type for: \(key.stringValue)"))
}
}
let json = ##"{ "age": "5", "name": 98 }"##
do {
let obj = try JSONDecoder().decode(Model.self, from: json.data(using: .utf8)!)
print(obj)
} catch {
print(error)
}

Resources