using decodable to capture array of nested objects in single line - ios

Below is my json response and the struct which I need to costruct out of it.
condition: I would not like to create any other struct apart from Response,Media and would like to parse in single line as specified below.
{
"name": "xxxx",
"title": "xxxxxxx",
"assets": [
{
"items": [
{
"id": "eeee",
"desc": "rrrrrr"
}, {
"id": "eeee",
}, {
"desc": "rrrrrr"
}]
}]
}
struct Response {
let name : String
let title : string
let items : [Media]
private enum codingKeys : String, CodingKey {
case name = "name"
case title = "title"
case items = "assets.items"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: codingKeys.self)
name = try container.decode(String.self, forKey: .name)
title = try container.decode(TrayLayout.self, forKey: .title)
items = try container.decode([Media].self, forKey: .items)
}
}

I managed to find a solution for your problem.
Considering following is sample json
let jsonString = """
{
"name": "xxxx",
"title": "xxxxxxx",
"assets": [
{
"items": [
{
"id": "id11",
"desc": "desc11"
}, {
"id": "id12",
}, {
"desc": "desc13"
}]
},{
"items": [
{
"id": "id21",
"desc": "desc21"
}, {
"id": "id22",
}, {
"desc": "desc23"
}]
}]
}
"""
Your structures will look as below
struct Media: Codable {
let id,desc: String?
enum CodingKeys: String, CodingKey {
case id,desc
}
}
struct Response: Decodable {
let name,title: String?
let items: [Media]?
enum CodingKeys: String, CodingKey {
case name,title,items
case assets = "assets"
}
// Decoding
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
title = try container.decode(String.self, forKey: .title)
// Here as the assets contains array of object which has a key as items and then again consist of a array of Media we need to decode as below
let assets = try container.decode([[String:[Media]]].self, forKey: .assets)
// At this stage will get assets with a array of array so we first map and retrive the items and then reduce them to one single array
items = assets.compactMap{$0[CodingKeys.items.rawValue]}.reduce([], +)
}
}
At the end it's time to use it as follows
let data = jsonString.data(using: .utf8)!
let myResponse = try! JSONDecoder().decode(Response.self, from: data)
Now you can access the data as below
myResponse.name
myResponse.title
myResponse.items
Hope this basic code helps you to achieve what you want to do. And then you can go ahead and do more nested parsing(s).
I would like to thanks Nic Laughter for such a detailed article; by referring which I managed to come with above solution.

Found a new way:
struct Media: Codable {
let id,desc: String?
enum CodingKeys: String, CodingKey {
case id,desc
}
}
struct Response: Decodable, CustomStringConvertible {
let name,title: String?
#NestedKey
let items: [Media]?
enum CodingKeys: String,NestableCodingKey {
case name,title,
case items = "assets/items"
}
}

You can try this:
struct Response: Decodable {
let name, title: String
let assets: [Assets]
}
struct Assets: Decodable {
let items: [Items]
}
struct Items: Decodable {
let id, desc: String
}
Then you can just decode it like this:
guard let response = try? JSONDecoder().decode(Response.self, from: data) else { print("Response not parsed"); return }

Related

Elegant solutions to decoding a JSON nested array in swift using decodable

Currently working with an API, and while I have successfully gotten it to decode the full result, I am only interested in the Entities/identifier portion. While I have gotten it working and get what I want/need I feel like this could be done better and more elegant and maybe in a single step. Any insight/suggestions appreciated.
JSON returned from API:
{
"count": 4596,
"entities": [
{
"facet_ids": [
"contact",
"siftery",
"investor",
"ipqwery",
"aberdeen",
"apptopia",
"semrush",
"company",
"rank",
"builtwith",
"bombora",
"key_event"
],
"identifier": {
"uuid": "468bef9f-2f50-590e-6e78-62e3adb05aa1",
"value": "Citi",
"image_id": "v1417152861/pdgwqt8ddecult5ktvdf.jpg",
"permalink": "citigroup",
"entity_def_id": "organization"
},
"short_description": "Citigroup is a diversified financial services holding company that provides various financial products and services."
},
{
"facet_ids": [
"contact",
"siftery",
"investor",
"apptopia",
"semrush",
"company",
"rank",
"builtwith",
"key_event"
],
"identifier": {
"uuid": "031a344b-c2b9-e60b-d950-1ae062026fde",
"value": "Citi",
"image_id": "yzlzhjqpparamrswaqa1",
"permalink": "citi-2",
"entity_def_id": "organization"
},
"short_description": "CITi is an NPO supporting the ICT sector in Western Cape."
},
{
"facet_ids": [
"contact",
"siftery",
"semrush",
"company",
"rank",
"builtwith",
"bombora"
],
"identifier": {
"uuid": "7ce45379-957c-49c5-bca2-c9ffd521f7da",
"value": "CITI",
"image_id": "qbkqndm7d0wgbogxjcrs",
"permalink": "citi-f7da",
"entity_def_id": "organization"
},
"short_description": "CITI trusted gateway to project-based change expertise that major organisations need to thrive, change and innovate."
}
]
}
Structs:
struct Entity: Decodable, Identifiable
{
var id: String
var companyName: String
var permalink: String
var imageID: String
init(from entity: Entities.Entity) throws
{
self.id = entity.identifier?.uuid ?? ""
self.companyName = entity.identifier?.value ?? ""
self.permalink = entity.identifier?.permalink ?? ""
self.imageID = entity.identifier?.image_id ?? ""
}
}
struct Entities: Decodable
{
var count:Int?
var entities: [Entity]?
struct Entity: Decodable
{
var facet_ids:[String]?
var identifier:Identifier?
var short_description:String?
}
struct Identifier:Decodable
{
var permalink:String?
var value:String?
var image_id:String?
var entity_def_id:String?
var uuid:String?
}
}
Call to decode:
if let data = data{
do {
let businessEntities = try decoder.decode(Entities.self, from: data)
let entities:[Entity] = try businessEntities.entities!.compactMap{
entity in
do
{
return try Entity(from: entity)
}
}
Thinking you are just interested in the Entities/identifier, you can simplify your model. Here's an example:
typealias Entities = [Entitie]
struct Entitie: Codable {
let facetIDS: [String]
let identifier: Identifier
let shortDescription: String
enum CodingKeys: String, CodingKey {
case facetIDS = "facet_ids"
case identifier
case shortDescription = "short_description"
}
}
struct Identifier: Codable {
let uuid, value, imageID, permalink: String
let entityDefID: String
enum CodingKeys: String, CodingKey {
case uuid, value
case imageID = "image_id"
case permalink
case entityDefID = "entity_def_id"
}
}
You can access entities object and decode it like this:
guard let data = data,
let jsonDict = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let entitiesObj = jsonDict["entities"] else { return }
do {
let entitiesData = try JSONSerialization.data(withJSONObject: entitiesObj)
let result = try JSONDecoder().decode(Entities.self, from: entitiesData)
} catch {
print(error.localizedDescription)
}
Obs: I created the CodingKeys because I use camelCase in my projects, you can left it in snake_case just remember to replace variables.

Swift decoding JSON which has two possible types for the same field [duplicate]

This question already has answers here:
How to deal with completely dynamic JSON responses
(3 answers)
Closed 2 years ago.
I am dealing with this JSON using Alamofire and Codable:
[
{
"pID": "37229890-dcd8-36c4-bb63-e7b174aafeb7",
"type": "FIRST",
"content": {
"id": "ff64",
"ret": {
"name": "A",
"logoUrl": "hpng"
},
"amo": {
"value": 120.00,
"currency": "EUR"
},
"s": {
"value": 1.20,
"currency": "EUR"
},
"datetime": "",
"p": [
{
"ti": "",
"pr": {
"value": 120.00,
"currency": "EUR"
},
"pic": "string"
}
]
}
},
{
"pID": "37229890-dcd8-36c4-bb63-e7b174aafeb7",
"type": "RATE",
"content": "Rate this app"
}
]
As you can see, te value of the type "content" can be a simple String or a Struct.
I have tried a custom decoder and having a top struct but I am not able to achieve a solution for this problem.
What is the best way to solve this problem?
Is this what you are expecting?
let json = """
[
{
"type": "type1",
"content": {
"id": "ff64",
"title": "a title"
}
},
{
"type": "type2",
"content": "Rate this app"
}
]
"""
struct Type1: Decodable {
let id: String
let title: String
}
typealias Type2 = String
enum Content: Decodable {
case type1(Type1)
case type2(Type2)
enum ContentType: String, Decodable {
case type1
case type2
}
enum Keys: String, CodingKey {
case type
case content
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Keys.self)
let type = try container.decode(ContentType.self, forKey: .type)
switch type {
case .type1:
let content = try container.decode(Type1.self, forKey: .content)
self = .type1(content)
case .type2:
let content = try container.decode(Type2.self, forKey: .content)
self = .type2(content)
}
}
}
let result = try JSONDecoder().decode([Content].self, from: json.data(using: .utf8)!)
You can apply AnyCodable custom decoder approach that can decode your needed types e.g.:
struct AnyCodable : Codable {
let value: Any
func encode(to encoder: Encoder) throws {
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let str = try? container.decode(String.self) {
value = str
} else if let content = try? container.decode(Content.self) {
value = content
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Value cannot be decoded!")
}
}
}
struct Content : Codable {
let id: String
}
struct Item : Codable {
let content: AnyCodable
}
How to use:
do {
let items = try JSONDecoder().decode([Item].self, from: json.data(using: .utf8)!);
for item in items {
if let content = item.content.value as? String {
print(content)
}
else if let content = item.content.value as? Content {
print(content.id)
}
}
}
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)
}

How to decode .keys from dictionary using Coadable

I have a below JSON response. And I want to fetch the allkeys of “data” which is [“rules”, “preference”, “goals”] using .keys method. But I couldn’t get the array of allkeys using .keys feature. I have attached my code snippet also. if you had faced this one, please suggest me to rid of this concern.
Although, I can get these allKeys using ObjectMapper and native Dictionary objects. I just need to know why I couldn't achieve this using Codable.
My json response
{
"statusCode": 200,
"status": "success",
"message": null,
"data": {
"rules": {
"goals": {
"min": "1",
"max": "3"
}
},
"preference": [
1,
2,
3
],
"goals": {
"total": 4,
"data": []
}
}
}
My code Snippet:
struct MeetingsDataModal: Codable {
let statusCode: Int?
let status: String?
let message: String?
let data: Results?
enum CodingKeys: String, CodingKey {
case statusCode = "statusCode"
case status = "status"
case message = "message"
case data = "data"
}
func allkeys() {
}
}
struct Results : Codable {
let rules: Rules?
let preference: [Preference]?
let goals: Goals?
enum CodingKeys: String, CodingKey {
case rules = "rules"
case preference = "preference"
case goals = "goals"
}
}
struct Rules : Codable {
}
struct Preference : Codable {
}
struct Goals : Codable {
}
My expectation
let content = try JSONDecoder().decode(MeetingsDataModal.self, from: (response as? Data)!)
print(content.data.keys)
But I am getting,
Value of type 'Results?' has no member 'keys'
Perhaps I am not understanding the question well but your "keys" are defined by your Codable protocol - so they are known. If you are using Swift 4.2+ you can take advantage of the CaseIterable protocol
struct Results: Codable {
let testOne: Int
let testTwo: String
enum CodingKeys: String, CodingKey, CaseIterable {
case testOne
case testTwo
}
}
Results.CodingKeys.allCases.map { $0.rawValue }
If you do need .keys. could add two-line code :
struct Results : Codable {
let rules: Rules?
let preference: [Preference]?
let goals: Goals?
enum CodingKeys: String, CodingKey {
case rules = "rules"
case preference = "preference"
case goals = "goals"
}
var keys : [String]{
return["rules", "preference","goals"]
}
}
Actually, if you don't like encoding/decoding way , we have another traditional JSON object can help you to handle unstructured JSON.
let obj = try JSONSerialization.jsonObject(with: response!, options: JSONSerialization.ReadingOptions.allowFragments)
print((((obj as! [String: Any?])["data"]) as! [String: Any?]).keys)
Here is one way to decode you data structure.
let d = """
{
"statusCode": 200,
"status": "success",
"message": null,
"data": {
"rules": {
"goals": {
"min": "1",
"max": "3"
}
},
"preference": [
1,
2,
3
],
"goals": {
"total": 4,
"data": []
}
}
}
""".data(using: .utf8)!
struct MeetingsDataModal: Decodable {
let statusCode: Int
let status: String
let message: String?
let data: Results
}
struct Results : Decodable {
let rules: Rules
let preference: [Int]
let goals: Goals
}
struct Rules : Decodable {
let goals : DirectData
}
struct DirectData : Decodable{
let min : String
let max : String
}
struct Goals : Decodable {
let total : Int
let data : [String]
}
let data0 = try JSONDecoder().decode(MeetingsDataModal.self, from: d)
print(data0)

Modelling an 'object' type to decode JSON

I'm making multiple calls to the Apple Music API which is returning an array of Resource objects. The Resource object has a property called attributes of type object which varies depending on whether it is a Song, Playlist, or Album.
How do I model the resource object so I can decode the JSON and create Song, Playlist, and Album objects?
struct Resource {
let id : String?
let type : String?
let href : String?
let attributes : "What goes here?"
}
Update 1:
I tried Joe's second recommendation and set up my structs like so:
struct Resource: Decodable{
enum Attribute {
case song(Song), playlist(Playlist)
}
let id : String?
let type : String?
let href : String?
let attributes: Attribute?
enum CodingKeys : CodingKey{
case id, type, href, attributes
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decode(String.self, forKey: .id)
type = try values.decode(String.self, forKey: .type)
href = try values.decode(String.self, forKey: .href)
switch type {
case "library-songs":
attributes = try .song(values.decode(Song.self, forKey: .attributes))
case "library-playlists":
attributes = try .playlist(values.decode(Playlist.self, forKey: .attributes))
default:
attributes = nil
}
}
}
struct Song : Decodable {
let playParams : PlayParams?
let trackNumber : Int?
let durationInMillis : Int?
let name : String?
let albumName : String?
let artwork : Artwork?
let contentRating : String?
let artistName : String?
}
struct Artwork : Decodable {
let width : Int?
let height : Int?
let url : String?
}
struct PlayParams : Decodable {
let id : String?
let kind : String?
let isLibrary : Bool?
}
The sample JSON looks like this for song:
{
"data": [
{
"id": "i.4YBNbl3IXVJQRM",
"type": "library-songs",
"href": "/v1/me/library/songs/i.4YBNbl3IXVJQRM",
"attributes": {
"albumName": "\"Awaken, My Love!\"",
"artwork": {
"width": 1200,
"height": 1200,
"url": "https://is5-ssl.mzstatic.com/image/thumb/Music71/v4/00/d0/d7/00d0d743-b0de-31d8-09eb-0796269bb555/UMG_cvrart_00044003187658_01_RGB72_1800x1800_16UMGIM77118.jpg/{w}x{h}bb.jpg"
},
"durationInMillis": 326933,
"playParams": {
"id": "i.4YBNbl3IXVJQRM",
"kind": "song",
"isLibrary": true
},
"artistName": "Childish Gambino",
"trackNumber": 6,
"name": "Redbone",
"contentRating": "explicit"
}
},
{
"id": "i.mmpeOrZiLqoKOv",
"type": "library-songs",
"href": "/v1/me/library/songs/i.mmpeOrZiLqoKOv",
"attributes": {
"albumName": "Funk Wav Bounces Vol. 1",
"artwork": {
"width": 1200,
"height": 1200,
"url": "https://is5-ssl.mzstatic.com/image/thumb/Music127/v4/8b/37/23/8b372308-f764-d03a-5bda-a7a456292547/886446469607.jpg/{w}x{h}bb.jpg"
},
"durationInMillis": 272659,
"playParams": {
"id": "i.mmpeOrZiLqoKOv",
"kind": "song",
"isLibrary": true
},
"artistName": "Calvin Harris",
"trackNumber": 4,
"name": "Rollin (feat. Future & Khalid)",
"contentRating": "explicit"
}
},
{
"id": "i.JL1aVxNtzmYDJG",
"type": "library-songs",
"href": "/v1/me/library/songs/i.JL1aVxNtzmYDJG",
"attributes": {
"albumName": "The Weekend (Funk Wav Remix) - Single",
"artwork": {
"width": 1200,
"height": 1200,
"url": "https://is4-ssl.mzstatic.com/image/thumb/Music118/v4/aa/d5/e5/aad5e5e3-dff5-7d8f-5747-b695ad9f2299/886446852157.jpg/{w}x{h}bb.jpg"
},
"durationInMillis": 171806,
"playParams": {
"id": "i.JL1aVxNtzmYDJG",
"kind": "song",
"isLibrary": true
},
"artistName": "SZA & Calvin Harris",
"trackNumber": 1,
"name": "The Weekend (Funk Wav Remix)"
}
}
]
}
When I decode the data using:
let resource = try? JSONDecoder().decode([Resource].self, from: data!)
print(resource!)
I get a fatal error: Unexpectedly found nil while unwrapping an Optional value
Update 2:
I figured out the issue. The extra layer on top was screwing up the decoding. I added another level:
struct Welcome: Codable {
let data: [Datum]
}
struct Datum: Codable {
let id, type, href: String
let attributes: Attributes
}
struct Attributes: Codable {
let albumName: String
let artwork: Artwork
let durationInMillis: Int
let playParams: PlayParams
let artistName: String
let trackNumber: Int
let name: String
let contentRating: String?
}
struct Artwork: Codable {
let width, height: Int
let url: String
}
struct PlayParams: Codable {
let id, kind: String
let isLibrary: Bool
}
This just works for songs but I will try reimplementing for both songs and playlists.

Resources