Swift how to express JSON in structs - json

I want to express the following JSON and convert to swift structs
1) I get error in the third line full_plan "comma is missing". I don't know why a comma is required? I need help fixing it
2) If that is fixed will the structs shown below is accurate to convert to JSON?
Please note: add_ons may be missing in the JSON for some plans, so second plan shown does not have add_ons.
Basically I am asking help to fix the JSON and the struct for swift.
{
"id": "100",
"plans":
[
"full_plan":
{
"plan":
[
{ "plan_type": "Legacy" },
{ "contract_duration_months": "12" }
],
"add_ons" :
[
{ "parking": "yes"},
{ "washerDryer": "no" }
]
},
"full_plan":
{
"plan":
[
{ "plan_type": "New" },
{ "contract_duration_months": "0" }
]
}
]
}
struct TopPlan : Decodable {
var uniqueId: String?
var Plans: [FullPlan]?
enum CodingKeys : String, CodingKey {
case uniqueId = "id"
case Plans = "plans"
}
}
struct FullPlan: Decodable {
var Plan: PlanJSON?
var freePlan: AddOnsJSON?
enum CodingKeys : String, CodingKey {
case pricedPlan = "plan"
case freePlan = "add_ons"
}
}
struct PlanJSON: Decodable {
var planType: String?
var duration: String?
enum CodingKeys : String, CodingKey {
case planType = "plan_type"
case duration = "contract_duration_months"
}
}
struct AddOnsJSON: Decodable {
var parking: String?
var washerDryer: String?
enum CodingKeys : String, CodingKey {
case parking = "parking"
case washerDryer = "washerDryer"
}
}

Short answer: your current JSON is invalid syntax.
You are using "full_plan" as a key (which would be fine if "plans" was an object) inside an array. Arrays in JavaScript (and thus in JSON) are unkeyed. You should either remove "full_plan" and just use the object that it refers to like "plans": [{}, {}, etc], or if you need to keep the object key wrap the entire item in curly braces such as "plans": [{ "full_plan": {}}, { "full_plan": {}}, etc]

You should build this up from the bottom (and if you post your question in a form that is executable in a Playground you will get answers faster). As it stands your plan JSON is unfortunate. Your struct states that you want a hash and you provide it with an array of hashes (which should be merged to get what you want). Try it like this:
import Cocoa
let jsonData = """
{ "plan_type": "Legacy",
"contract_duration_months": "12"
}
""".data(using: .utf8)!
struct PlanJSON: Decodable {
var planType: String?
var duration: String?
enum CodingKeys : String, CodingKey {
case planType = "plan_type"
case duration = "contract_duration_months"
}
}
do {
let plan = try JSONDecoder().decode(PlanJSON.self, from: jsonData)
print(plan)
} catch {
print(error)
}
That way you will be given enough information to further fix your JSON, but the rest looks ok.

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]!}
}
}

Issue Parsing / Decoding JSON from API Endpoint Into Struct Object

I am writing a Swift 5.x app using Alamofire 5 to get a list of files from an API I wrote. The API returns the file list as a JSON Data object. I want to then get this data into a struct I created. I am not able to get this working. Here is an example JSON string that my server sends over when you hit the API endpoint:
[{
"ID": "0d37ee7a-39bf-4eca-b3ec-b3fe840500b5",
"Name": "File01.iso",
"Size": 6148
}, {
"ID": "656e0396-257d-4604-a85c-bdd0593290cd",
"Name": "File02.iso",
"Size": 224917843
}, {
"ID": "275fdf66-3584-4899-8fac-ee387afc2044",
"Name": "File04.iso",
"Size": 5549504
}, {
"ID": "1c73f857-56b5-475b-afe4-955c9d2d87fe",
"Name": "File05.iso",
"Size": 15476866871
}, {
"ID": "bfebbca2-49de-43d7-b5d0-3461b4793b62",
"Name": "File06.iso",
"Size": 37254264
}]
I created the following Data Model in swift to hold this:
struct Files: Decodable {
let root: [File]
}
struct File: Decodable, Identifiable {
var id: UUID
var name: String
var size: Int
}
enum CodingKeys: String, CodingKey {
case id = "ID"
case name = "Name"
case size = "Size"
}
Then I used Alamofire 5.x to call the API endpoint and attempt to decode the JSON and place it into the object in question:
func getPackageFilesJSON() {
AF.request("http://localhost:8080/api/v1/list/pkgs").responseDecodable(of: Files.self) { response in
guard let serverFiles = response.value else {
print("Error Decoding JSON")
return
}
let self.serverFilesList = serverFiles
}
}
This fails. If I debugPrint the response I get this for the result:
[Result]: failure(Alamofire.AFError.responseSerializationFailed(reason: Alamofire.AFError.ResponseSerializationFailureReason.decodingFailed(error: Swift.DecodingError.typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Dictionary<String, Any> but found an array instead.", underlyingError: nil)))))
I have never been great at creating these data models and decoding JSON into them. I am sure I am missing something silly. I am hopeful that someone more knowledgable than me, or a second set of eyes can help me get this working.
Thanks,
Ed
There is no key root in the JSON. The root object is an array
Delete
struct Files: Decodable {
let root: [File]
}
and decode
AF.request("http://localhost:8080/api/v1/list/pkgs").responseDecodable(of: [File].self) { response in ...
and move the CodingKeys enum into the File struct
struct File: Decodable, Identifiable {
var id: UUID
var name: String
var size: Int
enum CodingKeys: String, CodingKey {
case id = "ID"
case name = "Name"
case size = "Size"
}
}

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?

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