How to create simple codable struct from complex json - json

I receive a complex json response from the API something similar to this.
{
"result": "success",
"count": 100,
"details": [{
"unnecessaryDetails": "something",
"area": {
"name": "Test1"
}
},
{
"unnecessaryDetails": "something",
"area": {
"name": "Test2"
}
},
{
"unnecessaryDetails": "something",
"area": {
"name": "Test3"
}
}
]
}
My struct is
struct Person {
var name: String
}
struct Response {
var result: String
var count: Int
var details: [Person]
}
I don't want to create properties for everything I receive from the response. I can't ask the backend developer to give the necessary details only. How to avoid unnecessary details and create struct with require details only?

You can skip intermediate arrays and dictionaries with nested containers.
struct Person : Decodable {
let name: String
}
struct Response : Decodable {
let result: String
let count: Int
let details: [Person]
enum CodingKeys: String, CodingKey { case result, count, details }
enum DetailCodingKeys: String, CodingKey { case area }
init(from decoder : Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
result = try container.decode(String.self, forKey: .result)
count = try container.decode(Int.self, forKey: .count)
var detailContainer = try container.nestedUnkeyedContainer(forKey: .details)
var people = [Person]()
while !detailContainer.isAtEnd {
let areaContainer = try detailContainer.nestedContainer(keyedBy: DetailCodingKeys.self)
let person = try areaContainer.decode(Person.self, forKey: .area)
people.append(person)
}
details = people
}
}
However the effort is much bigger than adding the extra struct
struct Response : Decodable {
let result: String
let count: Int
let details: [Detail]
}
struct Detail : Decodable {
let area : Person
}
struct Person : Decodable {
let name: String
}

You can use Codable to parse only property you want to parse and rest of all will be ignored, If you want to parse json in 2 separate models, you can follow this question's answer.
Is it possible to decode single level JSON into 2 separate models?

Related

Swift: How to decode a JSON with a dictionary of dictionaries with each key being an incrementing number value?

I am new to Swift and I was assigned a task to decode this JSON:
{
"data": {
"id": 1,
"elements": {
"E01": {
"title": "cars",
"items": ["honda", "toyota", "mercedes"],
"details": {
"id": 2,
"location": "toronto"
}
},
"E02": {
"title": "bagel types",
"items": ["plain", "grain", "toasted"],
}
}
}
}
I played around with this and I figured out how to decode this JSON when elements is an array of dictionaries with no incrementing key value. This is when elements looks like this:
"elements": [
{
"title": "cars",
"items": ["honda", "toyota", "mercedes"],
"details": {
"id": 2,
"location": "toronto"
}
},
{
"title": "bagel types",
"items": ["plain", "grain", "toasted"],
}
]
Here is my code on how to decode the JSON when elements look like the JSON directly above:
The classes I am decoding to:
public class StructureContainer: Codable {
let data: DataStructure
init(data: DataStructure) {
self.data = data
}
}
public class DataStructure: Codable {
let id: Int64?
let elements: [ElementStructure]?
init(id: Int64?, elements: [ElementStructure]?) {
self.id = id
self.elements = elements
}
}
public class ElementStructure: Codable {
let title: String?
let items: [String]?
let details: DetailStructure?
init(title: String?, items: [String]?, details: DetailStructure?) {
self.title = title
self.items = items
self.details = details
}
}
public class DetailStructure: Codable {
var id: Int64?
var location: String?
init(id: Int64?, location: String?) {
self.id = id
self.location = location
}
}
How I am decoding this:
func parseJSONLocally() {
do {
// JSONText is the variable that contains the JSON string
let jsonData = JSONText.data(using: .utf8)!
// dataInstance is the variable of type Structure that stores the parsed object.
dataInstance = try JSONDecoder().decode(StructureContainer.self, from: jsonData)
}
catch {
print("error", error)
}
}
I am able to successfully decode this when elements is an array of dictionaries with no incrementing key. However, I am lost on how to decode this when elements is a dictionary of dictionaries with each key incrementing.
I have found similar questions to this on StackOverflow but I don't think I have seen my exact problem. I am also new to Swift so I might be missing some knowledge somewhere. Any help is appreciated.
First of all Int64 makes no sense. On all modern computers Int64 is equal to Int. And declare only the properties as optional which can be nil
Assuming the dictionary keys don't matter implement init(from decoder to decode the dictionary version, decode [String:ElementStructure] and assign the dictionary values sorted by the keys to elements
public class DataStructure: Decodable {
let id: Int
let elements: [ElementStructure] // why not just Element?
private enum CodingKeys : String, CodingKey { case id, elements }
required public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
let elementData = try container.decode([String:ElementStructure].self, forKey: .elements)
let sortedkeys = elementData.keys.sorted{ $0.localizedStandardCompare($1) == .orderedAscending }
elements = sortedkeys.map{elementData[$0]!}
}
}

Complex JSON in Swift. How to get the data correctly. Different structures

Faced a difficult problem for me, when receiving data I don’t know how to decompose data in one array.
The responsible variable contains different types of data.
Do I get it right? I think in the initializer to go through the possible options and substitute the desired one? What type should the variable of this array be?
[
{
"id": 42,
"created_at": "2021-09-08T08:55:58.000000Z",
"updated_at": "2021-09-08T08:55:58.000000Z",
"link": "u4986",
"type": "u",
"responsible": {
"id": 4986,
"type": "management_company",
"email": "X#X.com",
"phone": "+0000000000",
"comment": null,
"first_name": "Alex",
"second_name": "Hook"
}
},
{
"id": 43,
"created_at": "2021-09-08T08:55:58.000000Z",
"updated_at": "2021-09-08T08:55:58.000000Z",
"link": "r14",
"type": "r",
"responsible": {
"id": 14,
"name": "manager",
"guard_name": "api",
"created_at": "2021-06-15T19:20:20.000000Z",
"updated_at": "2021-06-15T19:20:20.000000Z"
}
}
]
How to make an initializer for MyJson
struct MyJson: Codable {
let id: Int
let createdAt: String
let updatedAt: String
let link: String
let type: String
let responsible: Any
}
// MARK: - Responsible
struct User: Codable {
let id: Int
let type, email, phone, comment: String
let firstName, secondName: String
}
struct UserCategory: Codable {
let id: Int
let name, guardName, createdAt, updatedAt: String
}
In swift JSON parsing is pretty straightforward, build an object that reflects your JSON (I've just built an example based on your JSON here):
struct JsonExample: Decodable {
let id: Int
let responsible: [Responsible]
struct Responsible: Decodable {
let id: Int
let email: String
let guard_name: String
}
}
and then just decode it
let jsonData = "json_string".data(using: .utf8)!
do {
let decoded = try JSONDecoder().decode([JsonExample].self, from: jsonData)
} catch let error {
print(error.localizedDescription)
}
If you want to distinguish between nested objects you can use init and use a property inside your JSON to get the job done
init(from decoder : Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int.self, forKey: .id)
let type = try container.decode(String.self, forKey: .type)
if type == "a" {
let data = try container.decode([ResponsibleA].self, forKey: . responsible)
responsible = .responsibleA(data)
} else { // add better error handling
let data = try container.decode([ResponsibleB].self, forKey: . responsible)
responsible = .responsibleB(data)
}
}
Completed the task for a very long time. Finally I solved the problem. If you are facing this problem. You will find the solution here Indeterminate Types with Codable in Swift

Decode JSON in Swift with changing Key at beginning

I'm currently learning Swift and I wanted to create a little App, that gets Data about an Book via ISBN from the Openlibrary API.
Heres a Query that I use, to get Data:
https://openlibrary.org/api/books?bibkeys=ISBN:9783791504650&format=json&jscmd=data
Now the returning JSON looks like this:
{"ISBN:9783791504650": {"publishers": [{"name": "Dressler"}], "identifiers": {"isbn_13": ["9783791504650"], "openlibrary": ["OL8935767M"], "isbn_10": ["3791504657"], "librarything": ["1653"], "goodreads": ["292110"]}, "weight": "1.8 pounds", "title": "Tintenherz", "url": "https://openlibrary.org/books/OL8935767M/Tintenherz", "number_of_pages": 573, "cover": {"small": "https://covers.openlibrary.org/b/id/1027329-S.jpg", "large": "https://covers.openlibrary.org/b/id/1027329-L.jpg", "medium": "https://covers.openlibrary.org/b/id/1027329-M.jpg"}, "subject_places": [{"url": "https://openlibrary.org/subjects/place:italy", "name": "Italy"}], "subjects": [{"url": "https://openlibrary.org/subjects/fathers_and_daughters", "name": "Fathers and daughters"}, {"url": "https://openlibrary.org/subjects/characters_in_literature", "name": "Characters in literature"}, {"url": "https://openlibrary.org/subjects/magic", "name": "Magic"}, {"url": "https://openlibrary.org/subjects/storytelling", "name": "Storytelling"}, {"url": "https://openlibrary.org/subjects/fantasy", "name": "Fantasy"}, {"url": "https://openlibrary.org/subjects/bookbinding", "name": "Bookbinding"}, {"url": "https://openlibrary.org/subjects/fiction", "name": "Fiction"}, {"url": "https://openlibrary.org/subjects/books_and_reading", "name": "Books and reading"}, {"url": "https://openlibrary.org/subjects/bookbinders", "name": "Bookbinders"}, {"url": "https://openlibrary.org/subjects/authorship", "name": "Authorship"}, {"url": "https://openlibrary.org/subjects/characters_and_characteristics_in_literature", "name": "Characters and characteristics in literature"}, {"url": "https://openlibrary.org/subjects/juvenile_fiction", "name": "Juvenile fiction"}, {"url": "https://openlibrary.org/subjects/kidnapping", "name": "Kidnapping"}], "subject_people": [{"url": "https://openlibrary.org/subjects/person:meggie", "name": "Meggie"}, {"url": "https://openlibrary.org/subjects/person:mo", "name": "Mo"}, {"url": "https://openlibrary.org/subjects/person:dustfinger", "name": "Dustfinger"}, {"url": "https://openlibrary.org/subjects/person:capricorn", "name": "Capricorn"}, {"url": "https://openlibrary.org/subjects/person:basta", "name": "Basta"}, {"url": "https://openlibrary.org/subjects/person:mortola", "name": "Mortola"}, {"url": "https://openlibrary.org/subjects/person:fenoglio", "name": "Fenoglio"}, {"url": "https://openlibrary.org/subjects/person:elinor", "name": "Elinor"}, {"url": "https://openlibrary.org/subjects/person:resa", "name": "Resa"}, {"url": "https://openlibrary.org/subjects/person:the_shadow", "name": "The Shadow"}], "key": "/books/OL8935767M", "authors": [{"url": "https://openlibrary.org/authors/OL2704045A/Cornelia_Funke", "name": "Cornelia Funke"}], "publish_date": "2003", "ebooks": [{"formats": {}, "preview_url": "https://archive.org/details/tintenherz00funk", "availability": "restricted"}]}}
As you can see, the JSON starts with a Key, that includes the ISBN Number of given Book.
Currently, I have a File "Books.swift", which looks like this:
struct Books: Codable {
let isbn: Isbn?
enum CodingKeys: String, CodingKey {
case isbn
}
}
struct Isbn: Codable {
let publishers: [Publisher]?
let identifiers: Identifiers?
let weight: String?
let title: String?
let url: String?
let numberOfPages: Int?
let cover: Cover?
let subjectPlaces: [Author]?
let subjects: [Author]?
let subjectPeople: [Author]?
let key: String?
let authors: [Author]?
let publishDate: String?
let ebooks: [Ebook]?
enum CodingKeys: String, CodingKey {
case publishers = "publishers"
case identifiers = "identifiers"
case weight = "weight"
case title = "title"
case url = "url"
case numberOfPages = "number_of_pages"
case cover = "cover"
case subjectPlaces = "subject_places"
case subjects = "subjects"
case subjectPeople = "subject_people"
case key = "key"
case authors = "authors"
case publishDate = "publish_date"
case ebooks = "ebooks"
}
}
// MARK: - Author
struct Author: Codable {
let url: String?
let name: String?
enum CodingKeys: String, CodingKey {
case url = "url"
case name = "name"
}
}
// MARK: - Cover
struct Cover: Codable {
let small: String?
let large: String?
let medium: String?
enum CodingKeys: String, CodingKey {
case small = "small"
case large = "large"
case medium = "medium"
}
}
// MARK: - Ebook
struct Ebook: Codable {
let formats: Formats?
let previewURL: String?
let availability: String?
enum CodingKeys: String, CodingKey {
case formats = "formats"
case previewURL = "preview_url"
case availability = "availability"
}
}
// MARK: - Formats
struct Formats: Codable {
}
// MARK: - Identifiers
struct Identifiers: Codable {
let isbn13: [String]?
let openlibrary: [String]?
let isbn10: [String]?
let librarything: [String]?
let goodreads: [String]?
enum CodingKeys: String, CodingKey {
case isbn13 = "isbn_13"
case openlibrary = "openlibrary"
case isbn10 = "isbn_10"
case librarything = "librarything"
case goodreads = "goodreads"
}
}
// MARK: - Publisher
struct Publisher: Codable {
let name: String?
enum CodingKeys: String, CodingKey {
case name = "name"
}
}
I used an Online Converter for this, but it also included the ISBN for the book I used to get sample data.
And heres where the Request is started:
#IBAction func sendISBNSearchRequest(_ sender: Any) {
bookDataArray = [] //Empty Array, so there is no interfering old Data
let isbnUserInput: String = isbnInputfield.text! //Read UserInput from Textinputfield and save it into a String
self.loadingIndicator.startAnimating()
if (isbnUserInput.isNumeric && (isbnUserInput.count == 10 || isbnUserInput.count == 13)){
// Checks if user input only contains numbers as ISBN-Numbers only consists of numbers, not characters
// Also Check if it is a valid ISBN Number with 10 or 13 Numbers
let searchURL = "https://openlibrary.org/api/books?bibkeys=ISBN:\(isbnUserInput)&format=json&jscmd=data"
guard let url = URL(string: searchURL) else {
print("Error: cannot create URL")
return
}
let urlRequest = URLRequest(url: url)
let session = URLSession.shared
let task = session.dataTask(with: urlRequest) { data, response, error in
guard let data = data else {
return
}
let response = response as? HTTPURLResponse
if (response?.statusCode==200){
do{
let object = try JSONDecoder().decode(Books.self, from: data)
print(object)
DispatchQueue.main.async {
self.loadingIndicator.stopAnimating()
}
return
}catch{
print(error)
}
}
else{
self.loadingIndicator.stopAnimating()
}
}
task.resume()
}
else{
self.loadingIndicator.stopAnimating()
}
}
Is there any way to make it work for every request / ISBN-Number?
I'm still learning and the only way I see at the moment would be to create a case for every existing ISBN Number... :(
Alternatively write a custom initializer to separate the ISBN number
struct Books: Decodable {
let isbn: String
let book : Isbn
init(from decoder : Decoder) throws {
let container = try decoder.singleValueContainer()
let data = try container.decode([String:Book].self)
let key = data.keys.first!
isbn = key.components(separatedBy: ":").last!
book = data[key]!
}
}
do {
let object = try JSONDecoder().decode(Books.self, from: data)
print(object.isbn)
...
And you can get rid of the CodingKeys by adding the .convertFromSnakeCase key decoding strategy and making the names of the struct members conform to the conversion rules.
And it's bad practice to declare everything carelessly as optional.
PS:
Hey openlibrary.org, why not more appropriate
{"ISBN":"9783791504650","item":{"publishers":...
Use dictionary type [String: Isbn] instead of object of type Books:
let dictionary = try JSONDecoder().decode([String:Isbn].self, from: data)
print(dictionary["ISBN:\(isbnNumber)"])

Decoding a nested JSON Swift

I have to decode this type of JSON that is downloaded by a response.
The JSON is this, I need retrieve the "gallery" of all items
JSON: https://pastebin.com/KnEwZzxd
I have tried many solution but I am not able to create a Decode of this son.
I have posted the full code on pastebin, too.
{
"status": 200,
"data": {
"date": "2018-07-29T00:00:00.300Z",
"featured": [
{
"id": "5b56298d781e197186378f50",
"name": "Sun Tan Specialist",
"price": "1,500",
"priceIcon": "vbucks",
"priceIconLink": "https://image.fnbr.co/price/icon_vbucks.png",
"images": {
"icon": "https://image.fnbr.co/outfit/5b56298d781e197186378f50/icon.png",
"png": "https://image.fnbr.co/outfit/5b56298d781e197186378f50/png.png",
"gallery": "https://image.fnbr.co/outfit/5b56298d781e197186378f50/gallery.jpg",
"featured": "https://image.fnbr.co/outfit/5b56298d781e197186378f50/featured.png"
},
"rarity": "epic",
"type": "outfit",
"readableType": "Outfit"
},
{
"id": "5b562af2781e19db65378f5c",
"name": "Rescue Paddle",
"price": "800",
"priceIcon": "vbucks",
"priceIconLink": "https://image.fnbr.co/price/icon_vbucks.png",
"images": {
"icon": "https://image.fnbr.co/pickaxe/5b562af2781e19db65378f5c/icon.png",
"png": "https://image.fnbr.co/pickaxe/5b562af2781e19db65378f5c/png.png",
"gallery": "https://image.fnbr.co/pickaxe/5b562af2781e19db65378f5c/gallery.jpg",
"featured": false
},
"rarity": "rare",
"type": "pickaxe",
"readableType": "Pickaxe"
}
],
"daily": [
{
"id": "5ab1723e5f957f27504aa502",
"name": "Rusty Rider",
"price": "1,200",
"priceIcon": "vbucks",
"priceIconLink": "https://image.fnbr.co/price/icon_vbucks.png",
"images": {
"icon": "https://image.fnbr.co/glider/5ab1723e5f957f27504aa502/icon.png",
"png": "https://image.fnbr.co/glider/5ab1723e5f957f27504aa502/png.png",
"gallery": "https://image.fnbr.co/glider/5ab1723e5f957f27504aa502/gallery.jpg",
"featured": "https://image.fnbr.co/glider/5ab1723e5f957f27504aa502/featured.png"
},
"rarity": "epic",
"type": "glider",
"readableType": "Glider"
},
{
"id": "5b0e944bdb94f1a4bbc0a8e4",
"name": "Rambunctious",
"price": "500",
"priceIcon": "vbucks",
"priceIconLink": "https://image.fnbr.co/price/icon_vbucks.png",
"images": {
"icon": "https://image.fnbr.co/emote/5b0e944bdb94f1a4bbc0a8e4/icon.png",
"png": "https://image.fnbr.co/emote/5b0e944bdb94f1a4bbc0a8e4/png.png",
"gallery": "https://image.fnbr.co/emote/5b0e944bdb94f1a4bbc0a8e4/gallery.jpg",
"featured": false
}
]
}
}
Aside from posting the JSON Code itself, it would be useful to actually show an attempt as to how you have attempted to decode it as well ^_________^.
Anyway, the best way to tackle this issue is to use custom Structs and the Decodable Protocol to handle the JSON response.
From your JSON you will initially get a two values:
/// The Initial Response From The Server
struct Response: Decodable {
let status: Int
let data: ResponseData
}
From this we then map the 'data' to a struct called ResponseData:
/// The Data Object
struct ResponseData: Decodable{
let date: String
let featured: [Product]
let daily: [Product]
}
In this we have two variables which contain an array of identical struct which I have called Product:
/// The Product Structure
struct Product: Decodable{
let id: String
let name: String
let price: String
let priceIcon: String
let priceIconLink: String
let images: ProductImages
let rarity: String
let type: String
let readableType: String
}
Within this we have one variable which is a dictionary (images) which we then map to another struct:
/// The Data From The Product Images Dictionary
struct ProductImages: Decodable{
let icon: String
let png: String
let gallery: String
///The Featured Variable For The Product Images Can Contain Either A String Or A Boolean Value
let featured: FeaturedType
}
The issue you have with the ProductImages, is that the featured var sometimes contains a String but on others it contains a Bool value. As such we need to create a custom struct to handle the decoding of this to ensure we always get a String (I am probably not doing this right way so if someone has a better solution please say so):
/// Featured Type Returns A String Of Either The Boolean Value Or The Link To The JPG
struct FeaturedType : Codable {
let formatted: String
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
//1. If We Get A Standard Response We Have A String
let stringResult = try container.decode(String.self)
formatted = stringResult
} catch {
//2. On Occassions We Get An Bool
let boolResult = try container.decode(Bool.self)
formatted = String(boolResult)
}
}
}
Now that is the basic structure of your JSON so now you need to handle it. In this example I am loading the JSON from the MainBundle as I dont have the actual URL.
/// Loads & Decodes The JSON File
func retreiveJSON(){
//1. Load The JSON File From The Main Bundle
guard let jsonURL = Bundle.main.url(forResource: "sample", withExtension: ".json") else { return }
do{
//2. Get The Data From The URL
let data = try Data(contentsOf: jsonURL)
//3. Decode The JSON
let jsonData = try JSONDecoder().decode(Response.self, from: data)
//4. Extract The Data
extractDataFrom(jsonData)
}catch{
print("Error Processing JSON == \(error)")
}
}
In the above function you will notice I am calling the function extractDataFrom() which allows you to then do what you need to do with your data:
/// Extracts The Daily & Featured Products From The JSON
///
/// - Parameter jsonData: Response
func extractDataFrom(_ jsonData: Response){
//1. Get The Daily Products
let dailyProducts = jsonData.data.daily
dailyProducts.forEach { (product) in
print(product.id)
print(product.name)
print(product.price)
print(product.priceIcon)
print(product.priceIconLink)
print(product.images)
print(product.rarity)
print(product.type)
print(product.readableType)
}
//2. Get The Featured Products
let featuredProducts = jsonData.data.featured
featuredProducts.forEach { (product) in
print(product.id)
print(product.name)
print(product.price)
print(product.priceIcon)
print(product.priceIconLink)
print(product.images)
print(product.rarity)
print(product.type)
print(product.readableType)
}
}
If you wanted to save this data then all you would need to do is add the following variables under your class declaration e.g:
var featuredProducts = [Product]()
var dailyProducts = [Product]()
And in the extractDataFrom() function change the:
let dailyProducts
let featuredProducts
To:
dailyProducts = jsonData.data.daily
featuredProducts = jsonData.data.featured
Please note that this is a very crude example, and as noted, I may not be handling the 'featured' variable correctly.
Hope it helps...
Thanks to quicktype and other services converting valid json to Swift and other languages is simple. Editing it to fit your needs should be simple enough.
// To parse the JSON, add this file to your project and do:
//
// let welcome = try? JSONDecoder().decode(Welcome.self, from: jsonData)
import Foundation
struct Welcome: Codable {
let status: Int
let data: DataClass
}
struct DataClass: Codable {
let date: String
let featured, daily: [Daily]
}
struct Daily: Codable {
let id, name, price, priceIcon: String
let priceIconLink: String
let images: Images
let rarity, type, readableType: String?
}
struct Images: Codable {
let icon, png, gallery: String
let featured: Featured
}
enum Featured: Codable {
case bool(Bool)
case string(String)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let x = try? container.decode(Bool.self) {
self = .bool(x)
return
}
if let x = try? container.decode(String.self) {
self = .string(x)
return
}
throw DecodingError.typeMismatch(Featured.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for Featured"))
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .bool(let x):
try container.encode(x)
case .string(let x):
try container.encode(x)
}
}
}

Error in using Decoder swift Nested JSON

I want to parsing a JSON get from server but I have error i don't know why?!!
this is my struct:
struct MyResponse:Decodable {
let cats: [Cats]
}
struct Cats: Decodable {
let id: Int
let name: String
let menu: [Cats]
enum CodingKeys:String, CodingKey {
case name
case id
case menu = "SubMenu"
}
}
and create this extension :
extension MyResponse.Cats {
init(from decoder: Decoder) throws {
let valus = try decoder.container(keyedBy: CodingKeys.self)
name = try valus.decode(String.self, forKey: .name)
id = try valus.decode(Int.self, forKey: .id)
menu = try valus.decodeIfPresent(String.self, forKey: .menu)
}
}
I don't know how to parse this json. This json is very important because this is category of store of store. and this is my json value :
{
"cats": [
{
"id": 15,
"name": "کسب و کار ها",
"menu": [
{
"id": 16,
"name": "فروشگاهی",
"menu": [
{
"id": 17,
"name": "ورزشی"
},
{
"id": 18,
"name": "نوشت افزار"
}
]
},
{
"id": 19,
"name": "خدماتی",
"menu": ""
}
]
},
maybe in future menu now nil have sub menu
how to handle if menu is nil or have some data ??
Edit: and this line in init :
menu = try valus.decodeIfPresent(String.self, forKey: .menu)
have this error :
Cannot assign value of type 'String?' to type '[MyResponse.Cats]'
The value for key menu can be
An array of Cat
An empty string
The key is missing
So you need to write an custom initializer which handles the cases. The easiest way is to decode an array of Cat. If it fails assign an empty array.
Further you need an umbrella struct for the root object.
struct Root: Decodable {
let cats : [Cat] // it's recommended to name structs in singular form.
}
struct Cat : Decodable {
let id : Int
let name : String
let menu : [Cat]
enum CodingKeys : String, CodingKey { case name, id, menu }
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decode(String.self, forKey: .name)
id = try values.decode(Int.self, forKey: .id)
do {
menu = try values.decode([Cat].self, forKey: .menu)
} catch {
menu = [Cat]()
}
}
}
Alternatively declare menu optional
let menu : [Cat]?
and assign nil if the value is not [Cat]
... } catch { menu = nil }
decodeIfPresent does not work because the value can be two different types.
JSON example includes an entry that says "menu": "", whereas your structure assumes it is either another Menu instance, null, or completely absent.
As vadian pointed out, if your submenus sometimes come back as "", you can write a custom init(from:) method that manually parsed the JSON, as he illustrated. But better would be to fix whatever generated that JSON, so that the "menu":"" was not present at all, or if it was, it would be "menu":null (note, no quotes). It's better to fix the original problem in the JSON, rather than writing cumbersome JSON parsing init(from:) to handle the problem.
Assuming you fix the JSON, there is, as others have noted, another problem. Your Menu structure defines a property called submenu, but your JSON doesn't use that key. It uses menu. So, you either can:
Change the property name:
struct Menu: Codable {
let name: String
let id: Int
let menu: [Menu]?
}
or
Use CodingKeys enumeration:
struct Menu: Codable {
let name: String
let id: Int
let submenu: [Menu]?
enum CodingKeys: String, CodingKey {
case name, id
case submenu = "menu"
}
}
or
Change the JSON to use submenu key.
Assuming you fix the JSON, this demonstrates that you can parse it quite easily. This uses approach 2, shown above:
let data = """
{
"cats": [
{
"id": 15,
"name": "کسب و کار ها",
"menu": [
{
"id": 16,
"name": "فروشگاهی",
"menu": [
{
"id": 17,
"name": "ورزشی"
},
{
"id": 18,
"name": "نوشت افزار"
}
]
},
{
"id": 19,
"name": "خدماتی"
}
]
}
]
}
""".data(using: .utf8)!
struct Respons: Codable {
let cats: [Menu]
}
struct Menu: Codable {
let name: String
let id: Int
let submenu: [Menu]?
enum CodingKeys: String, CodingKey {
case name, id
case submenu = "menu"
}
}
do {
let object = try JSONDecoder().decode(Respons.self, from: data)
print(object)
} catch {
print(error)
}
Just make your Cats structure conform to Codable protocol and add an optional array [Cats]?.
//: Playground - noun: a place where people can play
import Foundation
struct MyResponse: Codable {
let cats: [Cats]
}
struct Cats: Codable {
let id: Int
let name: String
let menu: [Cats]?
}
// create json mock by encoding
let cats3 = Cats(id: 3, name: "3", menu: nil)
let cats2 = Cats(id: 2, name: "2", menu: nil)
let cats1 = Cats(id: 1, name: "1", menu: [cats2, cats3])
let myResponse = MyResponse(cats: [cats1])
let json = try! JSONEncoder().encode(myResponse)
print(String(bytes: json, encoding: String.Encoding.utf8)) // Optional("{\"cats\":[{\"id\":1,\"name\":\"1\",\"menu\":[{\"id\":2,\"name\":\"2\"},{\"id\":3,\"name\":\"3\"}]}]}")
// create category data by decoding json (your actual question)
do {
let myResponseAgain = try JSONDecoder().decode(MyResponse.self, from: json)
for cats in myResponseAgain.cats {
print(cats.id) // 1
print(cats.name) // 1
print(cats.menu) // Optional([__lldb_expr_30.Cats(id: 2, name: "2", menu: nil), __lldb_expr_30.Cats(id: 3, name: "3", menu: nil)])
print(cats.menu![0].id) // 2
print(cats.menu![0].name) // 2
print(cats.menu![0].menu) // nil
print(cats.menu![1].id) // 3
print(cats.menu![1].name) // 3
print(cats.menu![1].menu) // nil
}
} catch {
print("something went wrong")
}