Swift Codable to parse JSON with dynamic keys - ios

I am trying to parse the below JSON response, which has multiple dynamic keys,
{
"Nagaland": {
"districtData": {
"Dimapur": {
"confirmed": 1,
"lastupdatedtime": "",
"delta": {
"confirmed": 0
}
}
}
},
"Meghalaya": {
"districtData": {
"East Khasi Hills": {
"confirmed": 1,
"lastupdatedtime": "",
"delta": {
"confirmed": 0
}
}
}
}
}
I have written my Codable struct like below,,
struct IndianStateListModel: Codable {
// MARK: Properties
let state: [String: StateData]
}
struct StateData: Codable {
// MARK: Properties
var districtData: Inner?
/// Mapping Key Enum
private enum CodingKeys: String, CodingKey {
case districtData
}
}
struct Inner: Codable {
// MARK: Properties
let districts: [String: DistrictData]
}
struct DistrictData: Codable {
// MARK: Properties
var confirmed: Int?
var lastupdatedtime: String?
var delta: DailyConfirmedData?
/// Mapping Key Enum
private enum CodingKeys: String, CodingKey {
case confirmed, lastupdatedtime, delta
}
}
struct DailyConfirmedData: Codable {
// MARK: Properties
var confirmed: Int?
/// Mapping Key Enum
private enum CodingKeys: String, CodingKey {
case confirmed
}
}
It's called as,
let summary = try JSONDecoder().decode(IndianStateListModel.self, from: data)
But its returning nil
P.S.: related question regarding decodable Swift Codable with dynamic keys
Any solution, would be great, Thanks in advance

The Codable models that you must use to parse the above JSON data should be like,
Models:
struct StateData: Codable {
var districtData: [String:DistrictData]?
}
struct DistrictData: Codable {
var confirmed: Int?
var lastupdatedtime: String?
var delta: DailyConfirmedData?
}
struct DailyConfirmedData: Codable {
var confirmed: Int?
}
Parsing:
let summary = try JSONDecoder().decode([String:StateData].self, from: data)
Note: There is no need to explicitly create enum CodingKeys if the JSON keys exactly match the properties of the Codable type.

The fundamental issue is that IndianStateListModel has a property called states. But no such key appears in your JSON. I’d suggest parsing it with singleValueContainer. E.g. perhaps:
struct States: Decodable {
typealias StateName = String
let states: [StateName: Districts]
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
states = try container.decode([StateName: Districts].self)
}
}
struct Districts: Decodable {
typealias DistrictName = String
var districts: [DistrictName: DistrictData]
enum CodingKeys: String, CodingKey {
case districts = "districtData"
}
}
struct DistrictData: Decodable {
var confirmed: Int
var lastupdatedtime: String
var delta: DailyConfirmedData
}
struct DailyConfirmedData: Decodable {
var confirmed: Int?
}
And
do {
let result = try JSONDecoder().decode(States.self, from: data)
print(result)
} catch {
print(error)
}

Related

Codable: parse JSON with dynamic struct [duplicate]

I am trying to parse the below JSON response, which has multiple dynamic keys,
{
"Nagaland": {
"districtData": {
"Dimapur": {
"confirmed": 1,
"lastupdatedtime": "",
"delta": {
"confirmed": 0
}
}
}
},
"Meghalaya": {
"districtData": {
"East Khasi Hills": {
"confirmed": 1,
"lastupdatedtime": "",
"delta": {
"confirmed": 0
}
}
}
}
}
I have written my Codable struct like below,,
struct IndianStateListModel: Codable {
// MARK: Properties
let state: [String: StateData]
}
struct StateData: Codable {
// MARK: Properties
var districtData: Inner?
/// Mapping Key Enum
private enum CodingKeys: String, CodingKey {
case districtData
}
}
struct Inner: Codable {
// MARK: Properties
let districts: [String: DistrictData]
}
struct DistrictData: Codable {
// MARK: Properties
var confirmed: Int?
var lastupdatedtime: String?
var delta: DailyConfirmedData?
/// Mapping Key Enum
private enum CodingKeys: String, CodingKey {
case confirmed, lastupdatedtime, delta
}
}
struct DailyConfirmedData: Codable {
// MARK: Properties
var confirmed: Int?
/// Mapping Key Enum
private enum CodingKeys: String, CodingKey {
case confirmed
}
}
It's called as,
let summary = try JSONDecoder().decode(IndianStateListModel.self, from: data)
But its returning nil
P.S.: related question regarding decodable Swift Codable with dynamic keys
Any solution, would be great, Thanks in advance
The Codable models that you must use to parse the above JSON data should be like,
Models:
struct StateData: Codable {
var districtData: [String:DistrictData]?
}
struct DistrictData: Codable {
var confirmed: Int?
var lastupdatedtime: String?
var delta: DailyConfirmedData?
}
struct DailyConfirmedData: Codable {
var confirmed: Int?
}
Parsing:
let summary = try JSONDecoder().decode([String:StateData].self, from: data)
Note: There is no need to explicitly create enum CodingKeys if the JSON keys exactly match the properties of the Codable type.
The fundamental issue is that IndianStateListModel has a property called states. But no such key appears in your JSON. I’d suggest parsing it with singleValueContainer. E.g. perhaps:
struct States: Decodable {
typealias StateName = String
let states: [StateName: Districts]
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
states = try container.decode([StateName: Districts].self)
}
}
struct Districts: Decodable {
typealias DistrictName = String
var districts: [DistrictName: DistrictData]
enum CodingKeys: String, CodingKey {
case districts = "districtData"
}
}
struct DistrictData: Decodable {
var confirmed: Int
var lastupdatedtime: String
var delta: DailyConfirmedData
}
struct DailyConfirmedData: Decodable {
var confirmed: Int?
}
And
do {
let result = try JSONDecoder().decode(States.self, from: data)
print(result)
} catch {
print(error)
}

JSON objects in Swift 5

I'm trying to access data in a json api like below
"Products": [
{
"ProductName": "GR",
"ShortDescription": "General service epoxy mortar system that utilizes recycled glass and rapidly renewable soy based components.",
"PDSOverride": [
{
"FileName": "EcoLab Netherlands",
"FileUrl": "http://test.stonhard.com/media/2264/eco-lab-netherlands-usa-version.pdf"
},
{
"FileName": "General Dynamics.pdf",
"FileUrl": "http://test.stonhard.com/media/2060/general-dynamics.pdf"
}
]
}
]
And I'm modeling this in a struct like this below
struct Solutions: Codable, Identifiable {
let id = UUID()
let SectionTitle: String
let SectionImage: String
let ProductLines: [ProductLine]
}
struct ProductLine: Codable {
let System: String
let Products: [Product]
}
struct Product: Codable {
let ProductName: String
let ShortDescription: String
let PDSOverride: [PDSOverride]
}
struct PDSOverride: Codable {
let FileName: String
let FileUrl: String
}
struct SdsPdf: Codable {
let FileName: String
let FileUrl: String
}
struct GuideSpecPdf: Codable {
let FileName: String
let FileUrl: String
}
When I try to access the data, I get an error that says The data couldn't be read because it isn't in the correct format. I know it's a problem with my model because when I comment out PDSOverride, SdsPdf, and GuideSpecPdf it works, but obviously I don't have access to that data. How do I model my struct so I can pull in that data?
The problem is not in your model, your JSON is bad formatted as it says by the compiler your JSON needs to look like this:
[
{
"Products": [
{
"ProductName": "GR",
"ShortDescription": "General service epoxy mortar system that utilizes recycled glass and rapidly renewable soy based components.",
"PDSOverride": [
{
"FileName": "EcoLab Netherlands",
"FileUrl": "http://test.stonhard.com/media/2264/eco-lab-netherlands-usa-version.pdf"
},
{
"FileName": "General Dynamics.pdf",
"FileUrl": "http://test.stonhard.com/media/2060/general-dynamics.pdf"
}
]
}
]
}
]
Also, the model could be done in this way, I recommend you to use Quicktype (https://app.quicktype.io) the online version is good and they have a desktop one:
// MARK: - PurpleProduct
struct PurpleProduct: Codable {
let products: [ProductProduct]
enum CodingKeys: String, CodingKey {
case products = "Products"
}
}
// MARK: - ProductProduct
struct ProductProduct: Codable {
let productName, shortDescription: String
let pdsOverride: [PDSOverride]
enum CodingKeys: String, CodingKey {
case productName = "ProductName"
case shortDescription = "ShortDescription"
case pdsOverride = "PDSOverride"
}
}
// MARK: - PDSOverride
struct PDSOverride: Codable {
let fileName: String
let fileURL: String
enum CodingKeys: String, CodingKey {
case fileName = "FileName"
case fileURL = "FileUrl"
}
}
typealias Products = [PurpleProduct]
And to decoded you can use the JSONDecoder, I also recommend you to check your json in this page: https://jsonformatter.curiousconcept.com
Assuming your JSON is only missing the closing braces, you can parse it like so:
struct Entry: Codable {
let products: [Product]
enum CodingKeys: String, CodingKey {
case products = "Products"
}
}
struct Product: Codable {
let productName, shortDescription: String
let pdsOverride: [PDSOverride]
enum CodingKeys: String, CodingKey {
case productName = "ProductName"
case shortDescription = "ShortDescription"
case pdsOverride = "PDSOverride"
}
}
struct PDSOverride: Codable {
let fileName: String
let fileURL: String
enum CodingKeys: String, CodingKey {
case fileName = "FileName"
case fileURL = "FileUrl"
}
}
do {
let entries = try JSONDecoder().decode([Entry].self, from: data)
} catch {
print(error)
}

Handling null values from JSON

I am trying to call an API which gives me a json response which I am parsing by using JSON Decoder and decodable structs.
For example, JSON data is:
{
"value":[
{
"name":abc
},
{
"name":null
}
]
}
The structs are something like this:
struct output: Decodable {
let value: [value]
enum CodingKeys: String, CodingKey {
case value = "value"
}
}
struct value: Decodable {
let name: String
enum CodingKeys: String, CodingKey {
case name = "name"
}
}
I am not sure how to handle this scenario when I get null values because
the decoder gives error serializing JSON.
Replace
let name: String
with
let name: String?
{
"value":[
{
"name":"abc"
},
{
"name":null
}
]
}
Also no need for the CodingKeys if the keys are the same
struct Output: Decodable { // start structs with capital letter
let value: [Value]
}
struct Value: Decodable {
let name: String?
}

Can we reuse struct on Swift? Or is there any other way?

So I have a user JSON structure that goes like this:
- results: {
meta: {}
users: []
},
- status:
I want to get the user so the User model I implement to get the JSON is like this:
struct Response: Decodable {
let results: Result
let status: Int
}
struct Result: Decodable {
let meta: Meta
let users: [User]
}
struct Meta: Decodable {
let total_data: Int
let total_page: Int
}
struct User: Decodable {
let avatar_url: String
let created_at: String
let updated_at: String
let email: String
let id: Int
let name: String
let username: String
}
It is working, but when I have another JSON that the structure is similar, let say like this
- results: {
meta: {}
rooms: []
},
- status:
And when I create Room model, with another struct Response on it, it will cause error because it is duplicated declaration.
Is it possible to reuse struct in Swift? Or is there any convenient way to do this?
Thanks
You could use generics.
struct Response<T: Decodable>: Decodable {
let results: Result<T>
let status: Int
}
struct Result<T: Decodable>: Decodable {
let meta: Meta
let objects: [T]
}
struct Meta: Decodable {
let total_data: Int
let total_page: Int
}
struct User: Decodable {
let avatar_url: String
let created_at: String
let updated_at: String
let email: String
let id: Int
let name: String
let username: String
}
let userResponse: Response<User>?
You can try to use generics in the following way:
struct Response<ResultType> : Decodable where ResultType : Decodable {
let results: ResultType
let status: Int
}
and then use that struct via:
struct Result: Decodable {
let something: String
}
let decoder = JSONDecoder()
let data = "{\"status\":123,\"results\":{\"something\":\"hi\"}}".data(using: .utf8)!
let value = try! decoder.decode(Response<Result>.self, from: data) // will be of type Response<Result>
You have one option is to reuse Meta and Status
If you use sm
For user You can replace name
struct UserResult: Decodable {
let meta: Meta
let users: [User]
}
struct UserResponse: Decodable {
let results: UserResult
let status: Int
}
and for Room
struct RoomResult: Decodable {
let meta: Meta
let users: [Room]
}
struct RoomResponse: Decodable {
let results: RoomResult
let status: Int
}
Depending on your needs, you can use generics to compose the whole struct.
Start with your data models
struct User: Decodable {
let avatar_url: String
let created_at: String
let updated_at: String
let email: String
let id: Int
let name: String
let username: String
}
struct Room: Decodable {
let id: Int
let name: String
}
The goal is to get nice composed types.
typealias UserResponse = Response<Result<User, Meta>>
typealias RoomResponse = Response<Result<Room, Meta>>
The generics types to build from
struct Response<ResultType: Decodable>: Decodable {
let results: ResultType
let status: Int
}
struct Result<ItemType: Decodable, MetaType: Decodable>: Decodable {
let meta: MetaType
let items: [ItemType]
}
Even Meta is a separate part of the composition.
struct Meta: Decodable {
let total_data: Int
let total_page: Int
}
Now lets say we need a custom Meta for the Room response
struct PagedMeta: Decodable {
let current_page: Int
let page_count: Int
}
Here is the new type
typealias RoomPagedResponse = Response<Result<Room, PagedMeta>>

How to Decode Nested dictionary with nested arrays using Swift Decodable property?

The actual JSON,that I need to parse in swift4 is,
{
"class": {
"semester1": [
{
"name": "Kal"
},
{
"name": "Jack"
},
{
"name": "Igor"
}
],
"subjects": [
"English",
"Maths"
]
},
"location": {
"Dept": [
"EnglishDept",
],
"BlockNo": 1000
},
"statusTracker": {
"googleFormsURL": "beacon.datazoom.io",
"totalCount": 3000
}
}
The code that I'd tried but failed to execute is,
struct Class: Decodable {
let semester: [internalComponents]
let location: [location]
let statusTracker: [statusTracker]
enum CodingKeys: String, CodingKey {
case semester = "semester1"
case location = "location"
case statusTracker = "statusTracker"
}
}
struct location: Decodable {
let Dept: [typesSubIn]
let BlockNo: Int
}
struct statusTracker: Decodable {
let googleFormsURL: URL
let totalCount: Int
}
struct internalComponents: Decodable {
let semester1: [semsIn]
let subjects: [subjectsIn]
}
struct semsIn: Decodable {
let nameIn: String
}
struct subjectsIn: Decodable {
let subjects: String
}
struct Dept: Decodable {
let Depts: String
}
I know it's completely wrong can someone give the actual format? I'm actually confused with the format for "subjects".It's not compiling as a whole too.
There are many issues.
You are making a common mistake by ignoring the root object partially.
Please take a look at the JSON: On the top level there are 3 keys class, location and statusTracker. The values for all 3 keys are dictionaries, there are no arrays.
Since class (lowercase) is a reserved word, I'm using components. By the way please conform to the naming convention that struct names start with a capital letter.
struct Root : Decodable {
let components : Class
let location: Location
let statusTracker: StatusTracker
enum CodingKeys: String, CodingKey { case components = "class", location, statusTracker }
}
There are many other problems. Here a consolidated version of the other structs
struct Class: Decodable {
let semester1: [SemsIn]
let subjects : [String]
}
struct Location: Decodable {
let dept : [String]
let blockNo : Int
enum CodingKeys: String, CodingKey { case dept = "Dept", blockNo = "BlockNo" }
}
struct SemsIn: Decodable {
let name: String
}
struct StatusTracker: Decodable {
let googleFormsURL: String // URL is no benefit
let totalCount: Int
}
Now decode Root
do {
let result = try decoder.decode(Root.self, from: data)
} catch { print(error) }
It looks like you sterilize Class object in wrong way. It should looks like:
struct Class: Decodable {
let class: [internalComponents]
let location: [location]
let statusTracker: [statusTracker]
}
There are a few things here causing your issue.
You have no top level item, I added Response struct
Location, class and statusTracker are both at the same level, not under class.
In your class struct, your items are set as arrays but they aren't arrays
To debug these types of issues, wrap your decode in a do catch block and print out the error. it will tell you the reason it failed to parse
Try this code from my playground:
let jsonData = """
{
"class": {
"semester1": [{
"name": "Kal"
}, {
"name": "Jack"
}, {
"name": "Igor"
}],
"subjects": [
"English",
"Maths"
]
},
"location": {
"Dept": [
"EnglishDept"
],
"BlockNo": 1000
},
"statusTracker": {
"googleFormsURL": "beacon.datazoom.io",
"totalCount": 3000
}
}
""".data(using: .utf8)!
struct Response: Decodable {
let cls: Class
let location: Location
let statusTracker: statusTracker
enum CodingKeys: String, CodingKey {
case cls = "class"
case location
case statusTracker
}
}
struct Class: Decodable {
let semester: [SemesterStudents]
let subjects: [String]
enum CodingKeys: String, CodingKey {
case semester = "semester1"
case subjects
}
}
struct Location: Decodable {
let dept: [String]
let blockNo: Int
enum CodingKeys: String, CodingKey {
case dept = "Dept"
case blockNo = "BlockNo"
}
}
struct statusTracker: Decodable {
let googleFormsURL: URL
let totalCount: Int
}
struct SemesterStudents: Decodable {
let name: String
}
struct Dept: Decodable {
let Depts: String
}
do {
let result = try JSONDecoder().decode(Response.self, from: jsonData)
print(result)
} catch let error {
print(error)
}
Another approach is to create an intermediate model that closely matches the JSON, let Swift generate the methods to decode it, and then pick off the pieces that you want in your final data model:
// snake_case to match the JSON
fileprivate struct RawServerResponse: Decodable {
struct User: Decodable {
var user_name: String
var real_info: UserRealInfo
}
struct UserRealInfo: Decodable {
var full_name: String
}
struct Review: Decodable {
var count: Int
}
var id: Int
var user: User
var reviews_count: [Review]
}
struct ServerResponse: Decodable {
var id: String
var username: String
var fullName: String
var reviewCount: Int
init(from decoder: Decoder) throws {
let rawResponse = try RawServerResponse(from: decoder)
// Now you can pick items that are important to your data model,
// conveniently decoded into a Swift structure
id = String(rawResponse.id)
username = rawResponse.user.user_name
fullName = rawResponse.user.real_info.full_name
reviewCount = rawResponse.reviews_count.first!.count
}
}

Resources