ignore null object in array when parse with Codable swift - json

i'm parsing this API with swift Codable
"total": 7,
"searchResult": [
null,
{
"name": "joe"
"family": "adam"
},
null,
{
"name": "martin"
"family": "lavrix"
},
{
"name": "sarah"
"family": "mia"
},
null,
{
"name": "ali"
"family": "abraham"
}
]
with this PaginationModel:
class PaginationModel<T: Codable>: Codable {
var total: Int?
var data: T?
enum CodingKeys: String, CodingKey {
case total
case data = "searchResult"
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.total = try container.decodeIfPresent(Int.self, forKey: .total)
self.data = try container.decodeIfPresent(T.self, forKey: .data)
}
}
and User Model:
struct User: Codable {
var name: String?
var family: String?
}
i call jsonDecoder like this to parse API json:
let responseObject = try JSONDecoder().decode(PaginationModel<[User?]>.self, from: json)
now my problem is null in searchResult Array. it parsed correctly and when i access to data in paginationModel i found null in array.
how can i ignore all null when parsing API, and result will be an array without any null

In the first place, I would advise to always consider PaginationModel to be composed from arrays. You don't have to pass [User] as the generic type, you can just pass User. Then the parser can use the knowledge that it parses arrays and handle null automatically:
class PaginationModel<T: Codable>: Codable {
var total: Int?
var data: [T]?
enum CodingKeys: String, CodingKey {
case total
case data = "searchResult"
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.total = try container.decodeIfPresent(Int.self, forKey: .total)
self.data = (try container.decodeIfPresent([T?].self, forKey: .data))?.compactMap { $0 }
}
}
You might want to remove optionals here and use some default values instead:
class PaginationModel<T: Codable>: Codable {
var total: Int = 0
var data: [T] = []
enum CodingKeys: String, CodingKey {
case total
case data = "searchResult"
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.total = (try container.decodeIfPresent(Int.self, forKey: .total)) ?? 0
self.data = ((try container.decodeIfPresent([T?].self, forKey: .data)) ?? []).compactMap { $0 }
}
}

Simple solution, filter data after decoding
let responseObject = try JSONDecoder().decode(PaginationModel<[User?]>.self, from: data)
responseObject.data = responseObject.data?.filter{$0 != nil}

You may add an array type check within decode :
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.total = try container.decodeIfPresent(Int.self, forKey: .total)
self.data = try container.decodeIfPresent(T.self, forKey: .data)
//add the following:
if let array = self.data as? Array<Any?> {
self.data = ( array.compactMap{$0} as? T)
}
}

Note, you can just define the decodable variable that may be null/nil as [Float?] (or whatever type), with the optional '?' inside the array brackets.

Related

Swift - How to have multiple decoder method for a single model

struct Classroom: Codable {
let teacher: String
let id: Int
let status: Bool
init(from decoder: Decoder) throws {
...
...
}
}
Now I need a way to create Classroom instance with a simple String
{ "classroom": "Test" }//received from API
let classRoom = ClassRoom(teacher: "Test", id: 0, status: true)
Now I need to add a secondary decoder method which can create this classroom instance using the "classroom": "Test" data. The "Test" value should be used as value for "teacher" and other properties should contain default values.
I know I can decode the String value and create a new initializer. Is there a way to directly decode String to this model object?
if i understand well, i assume you have a bad json format like below
[
{
"teacher":"test",
"id":5,
"status":true
},
{
"classroom":"Test"
}
]
And you want to decode both objects, you can do the following
let data = """
[
{
"teacher": "test",
"id": 5,
"status": true
},
{
"classroom": "Test"
}
]
""".data(using: .utf8)!
struct Classroom: Codable {
let teacher: String
let id: Int
let status: Bool
private enum CodingKeys: CodingKey {
case teacher, id, status
}
private enum SecCodingKeys: CodingKey {
case classroom
}
init(from decoder: Decoder) throws {
let value = try decoder.container(keyedBy: CodingKeys.self)
let secValue = try decoder.container(keyedBy: SecCodingKeys.self)
let teacher_1 = try value.decodeIfPresent(String.self, forKey: .teacher)
let teacher_2 = try secValue.decodeIfPresent(String.self, forKey: .classroom)
teacher = teacher_1 ?? teacher_2 ?? ""
id = try value.decodeIfPresent(Int.self, forKey: .id) ?? 0
status = try value.decodeIfPresent(Bool.self, forKey: .status) ?? false
}
}
do {
let rooms = try JSONDecoder().decode([Classroom].self, from: data)
print(rooms.map(\.teacher))
} catch {
print(error)
}
and the result,
["test", "Test"]
Decode the second, nested case, as another type
struct SimpleClassroom: Decodable {
let classroom: String
}
and then have a computed property for mapping to the original type with default values
extension SimpleClassroom {
var classroomValue: Classroom {
Classroom(teacher: classroom, id: 0, status: false)
}
}
If "Test" is a valid description of classroom, and you want to go ahead and create the classroom, then you have a number of options.
If you know classrooms from a given API endpoint will always be in this string format, you can use the decoder's context dictionary to tell it up front which strategy to use to decode the classroom. If sometimes a classroom is a properly formed dictionary, and sometimes it's just a string, and you want to proceed either way, then you have to handle that case in the init(from:).
Either way you're looking at a custom init method. The second case, where you handle both types, would look like this:
init(from decoder: Decoder) throws {
// Do we have a single-value container?
do {
let container = try decoder.singleValueContainer()
let string = try container.decode(String.self)
self.teacher = string
self.id = 0
self.status = true
return
} catch {
// OK, it was a dictionary
}
let container = try decoder.container(keyedBy: CodingKeys.self)
self.teacher = try container.decode(String.self, forKey: .teacher)
self.id = try container.decode(Int.self, forKey: .id)
self.status = try container.decode(Bool.self, forKey: .status)
}
Given this made-up, horrible JSON:
[
"Test",
{ "teacher": "Mr Chips", "id": 0, "status": true }
]
let rooms = try JSONDecoder().decode([Classroom].self, from: data)
Gives you two valid Classroom types in an array.

Parsing Dynamic JSON Model in Swift [duplicate]

This question already has answers here:
Swift JSOn Decoding Error: Expected to decode Array<Any> but found a dictionary instead
(2 answers)
Closed 1 year ago.
My app must parse JSON data. The software (IceStats) that generates the data, though, generates JSON with two slightly different structures. Sometimes, the "source" value is an array of dictionaries and sometimes it just one dictionary. I can parse the JSON when it is one way or the other, but I don't know how to handle it both ways.
Here is the JSON in the Array version:
{
"icestats": {
"admin": "dontcontactme#localhost",
"host": "server.badradio.biz",
"location": "Airport",
"server_id": "Icecast 2.4.4",
"server_start": "Mon, 26 Apr 2021 12:50:47 -0500",
"server_start_iso8601": "2021-04-26T12:50:47-0500",
"source": [
{
"audio_info": "bitrate=128",
"bitrate": 128,
"genre": "Automation",
"listener_peak": 7,
"listeners": 0,
"listenurl": "http://server.badradio.biz:8000/ambient",
"server_description": "No show is running, tune in for selections from the venerable tape series \"Comfortable & Economical\"",
"server_name": "Comfortable & Economical",
"server_type": "audio/mpeg",
"server_url": "badradio.biz",
"stream_start": "Fri, 30 Apr 2021 06:51:49 -0500",
"stream_start_iso8601": "2021-04-30T06:51:49-0500",
"title": "Vol-15-A",
"dummy": null
},
{
"listeners": 0,
"listenurl": "http://server.badradio.biz:8000/stream",
"dummy": null
}
]
}
}
And here it is in the Dictionary version:
{
"icestats": {
"admin": "dontcontactme#localhost",
"host": "server.badradio.biz",
"location": "Airport",
"server_id": "Icecast 2.4.4",
"server_start": "Mon, 26 Apr 2021 12:50:47 -0500",
"server_start_iso8601": "2021-04-26T12:50:47-0500",
"source": {
"audio_info": "bitrate=128",
"bitrate": 128,
"genre": "Automation",
"listener_peak": 2,
"listeners": 0,
"listenurl": "http://server.badradio.biz:8000/ambient",
"server_description": "No show is running, tune in for selections from the venerable tape series \"Comfortable & Economical\"",
"server_name": "Comfortable & Economical",
"server_type": "audio/mpeg",
"server_url": "badradio.biz",
"stream_start": "Wed, 28 Apr 2021 02:18:31 -0500",
"stream_start_iso8601": "2021-04-28T02:18:31-0500",
"title": "Vol-13-A",
"dummy": null
}
}
}
Finally, here is my data model that handles the Array version:
import Foundation
struct StreamData: Decodable {
let icestats: IceStats
}
struct IceStats: Decodable {
let source: [Source]
}
struct Source: Decodable {
let server_name: String?
let stream_start: String?
let title: String?
let server_description: String?
let server_url: String?
let genre: String?
}
Any help is greatly appreciated. Ideally, I could just change the format of the JSON, but I am not able to.
You can add some custom decoding logic to try both cases:
struct IceStats: Decodable {
var source: [Source]
enum CodingKeys : CodingKey {
case source
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let elements = try? container.decode([Source].self, forKey: .source) {
source = elements
} else if let element = try? container.decode(Source.self, forKey: .source) {
source = [element]
} else {
throw DecodingError.dataCorruptedError(forKey: .source, in: container, debugDescription: "Must be either a single or multiple sources!")
}
}
}
But this can get really long if you also want to decode other properties in IceStats, because you'll have to manually write the decoding code for those too. To avoid this, you can use a property wrapper:
#propertyWrapper
struct MultipleOrSingle<Element: Decodable>: Decodable {
let wrappedValue: [Element]
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let elements = try? container.decode([Element].self) {
wrappedValue = elements
} else if let element = try? container.decode(Element.self) {
wrappedValue = [element]
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Container neither contains a single, nor multiple \(Element.self)!")
}
}
}
// Now you can just do this!
struct IceStats: Decodable {
#MultipleOrSingle
var source: [Source]
}
As per your array response your class looks like below and where you get response just decode that response class you will get all data
import Foundation
struct Response : Codable {
let icestats : Icestat?
enum CodingKeys: String, CodingKey {
case icestats = "icestats"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
icestats = Icestat(from: decoder)
}
}
struct Icestat : Codable {
let admin : String?
let host : String?
let location : String?
let serverId : String?
let serverStart : String?
let serverStartIso8601 : String?
let source : [Source]?
enum CodingKeys: String, CodingKey {
case admin = "admin"
case host = "host"
case location = "location"
case serverId = "server_id"
case serverStart = "server_start"
case serverStartIso8601 = "server_start_iso8601"
case source = "source"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
admin = try values.decodeIfPresent(String.self, forKey: .admin)
host = try values.decodeIfPresent(String.self, forKey: .host)
location = try values.decodeIfPresent(String.self, forKey: .location)
serverId = try values.decodeIfPresent(String.self, forKey: .serverId)
serverStart = try values.decodeIfPresent(String.self, forKey: .serverStart)
serverStartIso8601 = try values.decodeIfPresent(String.self, forKey: .serverStartIso8601)
source = try values.decodeIfPresent([Source].self, forKey: .source)
}
}
struct Source : Codable {
let audioInfo : String?
let bitrate : Int?
let dummy : AnyObject?
let genre : String?
let listenerPeak : Int?
let listeners : Int?
let listenurl : String?
let serverDescription : String?
let serverName : String?
let serverType : String?
let serverUrl : String?
let streamStart : String?
let streamStartIso8601 : String?
let title : String?
enum CodingKeys: String, CodingKey {
case audioInfo = "audio_info"
case bitrate = "bitrate"
case dummy = "dummy"
case genre = "genre"
case listenerPeak = "listener_peak"
case listeners = "listeners"
case listenurl = "listenurl"
case serverDescription = "server_description"
case serverName = "server_name"
case serverType = "server_type"
case serverUrl = "server_url"
case streamStart = "stream_start"
case streamStartIso8601 = "stream_start_iso8601"
case title = "title"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
audioInfo = try values.decodeIfPresent(String.self, forKey: .audioInfo)
bitrate = try values.decodeIfPresent(Int.self, forKey: .bitrate)
dummy = try values.decodeIfPresent(AnyObject.self, forKey: .dummy)
genre = try values.decodeIfPresent(String.self, forKey: .genre)
listenerPeak = try values.decodeIfPresent(Int.self, forKey: .listenerPeak)
listeners = try values.decodeIfPresent(Int.self, forKey: .listeners)
listenurl = try values.decodeIfPresent(String.self, forKey: .listenurl)
serverDescription = try values.decodeIfPresent(String.self, forKey: .serverDescription)
serverName = try values.decodeIfPresent(String.self, forKey: .serverName)
serverType = try values.decodeIfPresent(String.self, forKey: .serverType)
serverUrl = try values.decodeIfPresent(String.self, forKey: .serverUrl)
streamStart = try values.decodeIfPresent(String.self, forKey: .streamStart)
streamStartIso8601 = try values.decodeIfPresent(String.self, forKey: .streamStartIso8601)
title = try values.decodeIfPresent(String.self, forKey: .title)
}
}
decode it like
let jsonData = jsonString.data(using: .utf8)!
let response = try! JSONDecoder().decode(Response.self, from: jsonData)
print(response.icestats)

Swift decode JSON array of object based on property value type

How to decode an array of different JSON objects, where the same property on each object tells you what type to use to decode it:
let json =
"""
[
{
"#type": "FirstObject",
"number": 1
},
{
"#type": "SecondObject",
"name": "myName"
}
]
"""
Here is some code based on this similar answer which gets most of the way there, but fails because it doesn't know what CodingKeys are for .data:
struct FirstObject: MyData {
var dataType: String
var number: Int
enum CodingKeys: String, CodingKey {
case dataType = "#type"
case number
}
}
struct SecondObject: MyData {
var dataType: String
var name: String
enum CodingKeys: String, CodingKey {
case dataType = "#type"
case name
}
}
struct SchemaObj: Decodable
{
var dataType: String
var data: MyData
enum CodingKeys: String, CodingKey {
case data
case dataType = "#type"
}
enum ParseError: Error {
case UnknownSchemaType(Any)
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
dataType = try container.decode(String.self, forKey: .dataType)
switch dataType {
case "FirstObject":
data = try container.decode(FirstObject.self, forKey: .data)
case "SecondObject":
data = try container.decode(SecondObject.self, forKey: .data)
default:
throw ParseError.UnknownSchemaType(dataType)
}
}
}
do {
let data = Data(json.utf8)
let result = try JSONDecoder().decode([SchemaObj].self, from: data)
print(result)
} catch {
print(error)
}
Printed error is
keyNotFound(CodingKeys(stringValue: "data", intValue: nil), Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"data\", intValue: nil) (\"data\").", underlyingError: nil))
Thank you
You don't need the data coding key. Just decode the data property from the same decoder, based on the value of the JSON field:
enum CodingKeys: String, CodingKey {
case dataType = "#type"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
dataType = try container.decode(String.self, forKey: .dataType)
switch dataType {
case "FirstObject":
data = try FirstObject(from: decoder)
case "SecondObject":
data = try SecondObject(from: decoder)
default:
throw ParseError.UnknownSchemaType(dataType)
}
}
If you plan to add more types to that list, then the if/else if can can become hard to manage, you can use a lookup table to address this:
static let typeMapping: [String: MyData.Type] = [ "FirstObject": FirstObject.self ,
"SecondObject": SecondObject.self]
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let dataType = try container.decode(String.self, forKey: .dataType)
guard let classToDecode = Self.typeMapping[dataType] else {
throw ParseError.UnknownSchemaType(dataType)
}
self.dataType = dataType
self.data = try classToDecode.init(from: decoder)
}

Decoding this kind of JSON with Codable

I have a json that looks like this:
let jsonString = """
{
"data": [
{
"name": "Apple",
"price": 44,
"available": true
},
{
"name": "Pear",
"price": 27,
"available": false
}
],
"some_stuff": [],
"some_other_stuff": []
}"""
My goal is to parse the content of the data array, specifically in a struct that doesn't care about the availibility attribute. I don't care about the some_stuff and some_other_stuff returned is that json.
First question: Can I ignore these properties event if they are part of the response at the same level as "data" which I'm interested in? If so, how do I represent and empty array of a type I ignore because I only get empty arrays? (For that part, I chose to represent it as an array of arbitrary chosen type Int?)
I created a struct:
struct Fruit: Codable {
let name: String
let price: Int
}
And a kind of super struct above:
struct WholeResponse: Codable {
let data: [Fruit]
let someStuff: [Int?]
let someOtherStuff: [Int?]
enum CodingKeys: String, CodingKey {
case data
case someStuff = "some_stuff"
case someOtherStuff = "some_other_stuff"
}
struct Fruit: Codable {
let name: String
let price: Int
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let data = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .data)
name = try data.decode(String.self, forKey: .name)
price = try data.decode(Int.self, forKey: .price)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
var data = container.nestedContainer(keyedBy: CodingKeys.self, forKey: .data)
try data.encode(name, forKey: .name)
try data.encode(price, forKey: .price)
}
}
But I found myself pretty stuck after doing this:
let jsonData = jsonString.data(using: .utf8)!
let jsonDecoder = JSONDecoder()
let fruits = try jsonDecoder.decode(WholeResponse.self, from: jsonData)
I think I am missing something about the representation of my data and perhaps complicating the thing but can you help me get this data array parsed?
First just delete everything you don't want:
struct WholeResponse: Codable {
let data: [Fruit]
struct Fruit: Codable {
let name: String
let price: Int
}
}
Then, similar to what you had before, say
let response = try jsonDecoder.decode(WholeResponse.self, from: jsonData)
let fruits = response.data

swift4 encoding decoding for model class of nested json parsing

I have a model class of swift which was created based on a nested json response, it follows like below
struct RootClass : Codable {
let details : String?
let itemCount : Int?
let list : [List]?
enum CodingKeys: String, CodingKey {
case details = "Details"
case itemCount = "ItemCount"
case list = "List"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
details = try values.decodeIfPresent(String.self, forKey: .details)
itemCount = try values.decodeIfPresent(Int.self, forKey: .itemCount)
list = try values.decodeIfPresent([List].self, forKey: .list)
}
}
struct List : Codable {
let companyID : Int?
let employeeCount : Int?
let employeeUser : EmployeeUser?
enum CodingKeys: String, CodingKey {
case companyID = "CompanyID"
case employeeCount = "EmployeeCount"
case employeeUser = "EmployeeUser"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
companyID = try values.decodeIfPresent(Int.self, forKey: .companyID)
employeeCount = try values.decodeIfPresent(Int.self, forKey: .employeeCount)
employeeUser = try EmployeeUser(from: decoder)
}
}
struct EmployeeUser : Codable {
let mobileNumber : String?
let name : String?
enum CodingKeys: String, CodingKey {
case mobileNumber = "MobileNumber"
case name = "Name"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
mobileNumber = try values.decodeIfPresent(String.self, forKey: .mobileNumber)
name = try values.decodeIfPresent(String.self, forKey: .name)
}
}
and my json response is
{
"Details": null,
"List": [
{
"CompanyID": 140,
"EmployeeUser": {
"Name": " raghu2",
"MobileNumber": "8718718710"
},
"EmployeeCount": 0
},
{
"CompanyID": 140,
"EmployeeUser": {
"Name": "new emp reg",
"MobileNumber": "1"
},
"EmployeeCount": 0
}
],
"ItemCount": 0
}
I am trying to parse it like
guard let data = data else { return }
do {
let decoder = JSONDecoder()
let gitData = try decoder.decode(RootClass.self, from: data)
print(gitData.itemCount ?? "")
print(gitData.list![0].employeeUser?.mobileNumber ?? "")
}
catch let err {
print("Err", err)
}
I am able to get the values of root class and list but I am getting nil values under employee user section.
Your code a few problems:
All your keys are optional. The vendor API will tell you what keys are always present and which one are optional. Follow that.
decodeIfPresent will silently fail if it cannot decode a key. When debugging your app, you want things to fail with a bang so you can fix the error before going to production.
You wrote way more code than needed. All those init(from decoder: ) functions are not needed. One one did cause your problem.
Your problem was caused by this line:
struct List : Codable {
init(from decoder: Decoder) throws {
...
employeeUser = try EmployeeUser(from: decoder)
}
}
You are asking Swift to decode to same JSON to a List and a EmployeeUser object. Obviously, that's not valid. But when you decode list inside RootClass, you call decodeIfPresent:
// In Rootclass
list = try values.decodeIfPresent([List].self, forKey: .list)
This call silently failed and you never knew what the problem was!
Solution
Change how you initialize employeeUser to this:
employeeUser = try values.decodeIfPresent(EmployeeUser.self, forKey: .employeeUser)
But the most elegant solution is to delete all those init(from decoder: ). The compiler will synthesize them for you.
And finally, fix those optionals!