Decode JSON in Swift with changing Key at beginning - json

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)"])

Related

Swift Dynamic json Values Reading and Writing

Problem Stuck On
I am trying to be able to read out my json file data to the console log for testing so I can use it later on.
Im not sure how to finish off my other structs due to varying data size and possible bad json format.
Once I finish that I believe I would need to use a for loop for read the varying size of data from "M", "S" and "WP" (This part shouldn't be complicated I believe)
Possible Things to Consider
I want to write and add data to "M" "S" "WP"
The data amount for ("M", "S") could be any number of String Array data objects
The data in "WP" Might need a different format I would like to add a name("abc") with a Int array containing any number of data points
Note: My Json Format Might Be Wrong in Some Areas concerning MP and WP
Swift Code To Grab Data
import Foundation
struct UserDay: Codable {
let mp: UserMP
let wp: UserWP
}
struct UserMP: Codable {
let m: [UserM]
let s: [UserS]
}
struct UserM : Codable {
let title: String
let description: String
let time: String
}
struct UserS : Codable {
let title: String
let description: String
let time: String
}
struct UserWP: Codable {
let wp: [WPData]
}
struct WPData: Codable {
let title: String
let values: [Int]
}
class LogDataHandler {
public func grabJSONInfo(){
guard let jsonURL = Bundle(for: type(of: self)).path(forResource: "LogData", ofType: "json") else { return }
guard let jsonString = try? String(contentsOf: URL(fileURLWithPath: jsonURL), encoding: String.Encoding.utf8) else { return }
// Print Info for TESTING
var year: UserDay?
do {
year = try JSONDecoder().decode(UserDay.self, from: Data(jsonString.utf8))
} catch {
print("ERROR WHEN DECODING JSON")
}
guard let results = year else {
print("YEAR IS NIL")
return
}
print(results)
}
}
JSON Example Data Below
{
"01/01/2020": {
"MP" : {
"M" : [
{"title" : "m1", "description" : "1", "time" : "12:30pm"},
{"title" : "m2", "description" : "2", "time" : "1:30pm"},
{"title" : "m3", "description" : "3", "time" : "2:30pm"}
],
"S" : [
{"title" : "s1", "description" : "1", "time" : "1pm"}
]
},
"WP" : [
{ "title" : "abc", "values" : [12, 10, 6]},
{ "title" : "def", "values" : [8]}
]
},
"01/29/2020": {
"MP" : {
"M" : [{"title" : "m1", "description" : "1", "time" : "12:30pm"}],
"S" : [{"title" : "s1", "description" : "1", "time" : "12:30pm"}]
},
"WP" :[{ "title" : "def", "values" : [8]}]
}
}
Based on the comments and our chat, this seems to be a question of the right way to construct the Swift models and the JSON object.
Based on your (updated) JSON, you might want to decode your data into a [String: UserDay] - a dictionary with a date string as key and UserDay as a value.
First, WP property in your JSON is just an array of objects (that map to WPData), so it's best to change your UserDay.wp to be [WPData] instead of UserWP:
struct UserDay: Codable {
let mp: UserMP
let wp: [WPData] // <-- changed
}
Second, some of your models' properties don't match directly to what's in JSON because keys-properties mapping is case sensitive. You can explicitly define CodingKeys to map them:
struct UserDay: Codable {
let mp: UserMP
let wp: [WPData]
enum CodingKeys: String, CodingKey {
case mp = "MP", wp = "WP"
}
}
struct UserMP: Codable {
let m: [UserM]
let s: [UserS]
enum CodingKeys: String, CodingKey {
case m = "M", s = "S"
}
}
Now you're ready to decode [String: UserDay]:
let userDays = try JSONDecoder().decoder([String: UserDay].self, from: jsonData)
let userDay = userDays["01/29/2020"]
Of course, working with String instead of Date isn't very convenient. Unfortunately, Dictionary's conformance to Codable only supports Int or String as keys (AFAIK).
So, let's do a manual decoding into a new root object UserData that works with Dates:
struct UserData: Codable {
var userDays: [Date: UserDay]
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let dict = try container.decode([String: UserDay].self)
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MM/dd/yyyy"
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
// decode (String, UserDay) pairs into an array of (Date, UserDay)
let pairs = dict.compactMap { (key, value) -> (Date, UserDay)? in
guard let date = dateFormatter.date(from: key) else { return nil }
return (date, value)
}
// uniquing is used just in case there non unique keys
self.userDays = Dictionary(pairs, uniquingKeysWith: {(first, _) in first})
}
}
Now, we can decode into this UserData object:
let userData = try JSONDecoder().decode(UserData.self, from: jsonData)
let todaysData = userData.userDays[Date()]

How to create simple codable struct from complex 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?

parsing json any type with Codable

I am sorry if this question is answered many times but am looking for my own specific json issue that's why am posting this, This is my json its an array list of notifications and in the extra object you can see it has contract object, Now this object is changing it can be campaign or feedback in the 2nd index of the json array how do I make Codable struct for this to decode this type of jsonHere is my struct, I cannot do Any type in Codable
struct Alert: Decodable {
var id: Int?
var isUnread: Bool?
var userId: Int?
var message: String?
var notificationType: String?
var extra: Any?
"value": [
{
"id": 153,
"is_unread": true,
"user_id": 3,
"message": "Contract offered from JohnWick. Click here to see the details.",
"notification_type": "Contract",
"extra": {
"contract": {
"id": 477,
"likes": 0,
"shares": 0,
"account_reach": 0.0,
"followers": 0,
"impressions": 0.0,
"description": "Fsafasd",
"budget": 0.0,
"start_date": null,
"end_date": null,
"status": "pending",
"is_accepted": false,
"brand_id": 443,
"influencer_id": 3,
"proposal_id": 947,
"created_at": "2019-11-09T17:40:57.646Z",
"updated_at": "2019-11-09T17:40:57.646Z",
"contract_fee": 435345.0,
"base_fee": 5000.0,
"transaction_fee": 43534.5,
"total_fee": 483879.5,
"infuencer_completed": false,
"brand_completed": false,
"comments": 0
}
}
},
{
"id": 152,
"is_unread": true,
"user_id": 3,
"message": "Message from JohnWick. Click here to check your inbox.",
"notification_type": "Message",
"extra": {
"message": {
"id": 495,
"body": "Uuhvh",
"read": false,
"conversation_id": 42,
"user_id": 3,
"created_at": "2019-11-08T13:44:02.055Z",
"updated_at": "2019-11-08T13:44:02.055Z"
}
}
},
]
As you can see it can be message, or campaign , or contract or feedback so how do I parse this or make model for this with CodingKeys Codable
A simple solution would be to make a struct called Extra that has four optional properties for each of the cases that you have.
struct Extra: Codable {
let contract: Contract?
let message: Message?
let feedback: Feedback?
let campaign: Campaign?
}
As long as each of message, campaign, contract, and feedback have fixed responses then you should be able to make structs for them that conform to Codable.
You don't mean Any here. As you said, extra can be "message, or campaign, or contract or feedback." It can't be a UIViewController or a CBPeripheral. It's not "anything." It's one of 4 things.
"One of X things" is an enum:
enum Extra {
case contract(Contract)
case campaign(Campaign)
case message(Message)
case feedback(Feedback)
}
To make it Decodable, we just need to look for the right key:
extension Extra: Decodable {
enum CodingKeys: CodingKey {
case contract, campaign, message, feedback
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
// Try to decode each thing it could be; throw if nothing matches.
if let contract = try? container.decode(Contract.self, forKey: .contract) {
self = .contract(contract)
} else if let campaign = try? container.decode(Campaign.self, forKey: .campaign) {
self = .campaign(campaign)
} else if let message = try? container.decode(Message.self, forKey: .message) {
self = .message(message)
} else if let feedback = try? container.decode(Feedback.self, forKey: .feedback) {
self = .feedback(feedback)
} else {
throw DecodingError.valueNotFound(Self.self,
DecodingError.Context(codingPath: container.codingPath,
debugDescription: "Could not find extra"))
}
}
}
in this article, you can find a way that support any type with the codable, so it will not matter if JSON returns String or Int:
anycodableValue
It's not Any type, you got four different but predictable types.
With reference to Rob's answer you can get rid of the decoding attempts in the if - else if chain if you decode notificationType as enum and decode the subtypes according to its value
enum NotificationType : String, Decodable {
case contract = "Contract", message = "Message", campaign = "Campaign", feedback = "Feedback"
}
enum Extra {
case contract(Contract)
case campaign(Campaign)
case message(Message)
case feedback(Feedback)
}
struct Alert: Decodable {
let id: Int
let isUnread: Bool
let userId: Int
let message: String
let notificationType: NotificationType
let extra: Extra
private enum CodingKeys : String, CodingKey { case id, isUnread, userId, message, notificationType, extra}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
isUnread = try container.decode(Bool.self, forKey: .isUnread)
userId = try container.decode(Int.self, forKey: .userId)
message = try container.decode(String.self, forKey: .message)
notificationType = try container.decode(NotificationType.self, forKey: .notificationType)
switch notificationType {
case .contract:
let contractData = try container.decode([String:Contract].self, forKey: .extra)
extra = .contract(contractData[notificationType.rawValue.lowercased()]!)
case .campaign:
let campaignData = try container.decode([String:Campaign].self, forKey: .extra)
extra = .campaign(campaignData[notificationType.rawValue.lowercased()]!)
case .message:
let messageData = try container.decode([String:Message].self, forKey: .extra)
extra = .message(messageData[notificationType.rawValue.lowercased()]!)
case .feedback:
let feedbackData = try container.decode([String:Feedback].self, forKey: .extra)
extra = .feedback(feedbackData[notificationType.rawValue.lowercased()]!)
}
}
}

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")
}

Nested Swift 4 Codable Struct uses CodingKeys irregularly

I have some JSON that I'm trying to decode with a Swift struct that conforms to the Codable protocol. The main struct doesn't seem to want to recognize the codingKey alias for thread_type, but happily consumes the explicitly named thread_id attribute. The struct in question is below:
struct Message: Codable {
var id: String?
var type: String?
var thread_id: String?
var threadType: String?
var sender: User?
var body: String?
var media: String?
var sentAt: Double?
var unread: Bool?
var status: String?
var url: String?
enum codingKeys: String, CodingKey {
case id
case type
case thread_id
case threadType = "thread_type"
case sender
case body
case media
case sentAt = "sent_at"
case unread
case status
case url
}
}
The JSON that I'm trying to parse:
let json =
"""
{
"id": "Jvbl6LY",
"type": "sms",
"thread_id": "60LrVL7",
"thread_type": "578a",
"delay_until": null,
"sender": {
"id": "EVkdNBx",
"type": "user",
"first_name": "Jerry",
"last_name": "Ward",
"mobile_number": "123-456-7890",
"profile_image_url": "",
"is_online": false,
"email": "jerryw+demo#jypsee.com"
},
"body": "Here is a picture of our Coquille Suite. Let me know if you would like a reservation?",
"media": "",
"sent_at": 1509133604000.167,
"unread": false,
"status": "",
"url": "https://connect-staging.jypsee.com/api/sms/threads/60LrVL7/history/Jvbl6LY/"
}
"""
And finally the decoder call itself:
let decoder = JSONDecoder()
let data = json.data(using: .utf8)!
do {
let message = try decoder.decode(Message.self, from: data)
print(message.thread_id)
print(message.threadType)
print(message.sender?.firstName)
print(message.sender?.lastName)
} catch {
print(error)
}
The message.thread_id prints Optional("60LrVL7")\n" which is expected.
The message.threadType prints nil\n, which is not expected. Even more bizarre is the fact that message.sender?.firstName and message.sender?.lastName both print "Optional("Jerry")\n" and "Optional("Ward")\n" respectively. Which means the nested User Codable Struct CodingKey IS working. I'm really at a loss as to why there's such an inconsistency in decoding.
Xcode Playground Gist is available here
Apple doc clearly states in the first sentence on the paragraph below. A special enumeration named 'CodingKeys' is necessary. (I had the similar problem and took me quite some time to find out).
https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types