Decode nested JSON as string using JSONDecoder() - swift

I have a JSON data structure representing Animals. e.g.:
{
"id": "3",
"type": "dog",
"properties": {
"tail_color": "black",
"paw_color": "white"
}
}
{
"id": "5",
"type": "fish",
"properties": {
"fin_color": "black",
"num_teeth": "3"
"max_speed": "27"
}
}
id and type are consistent for all animals but the keys within properties change depending on type.
I want to parse these animals into struct AnimalModel. Because properties is inconsistent, I want to store this sub-json as string (and process it later):
struct AnimalModel: Identifiable, Codable {
let id: String
let type: String
let properties: String // JSON, content and keys depending on type
enum CodingKeys: String, CodingKey {
case id
case type
case properties
}
}
// data .. JSON-string
guard let newAnimal = try? JSONDecoder().decode(AnimalModel.self, from: data) else {
return
}
The JSONDecoder() however fails here. How can I parse properties as string into AnimalModel?

Related

Having problems mapping down the json data

I am having problems with mapping down some json data that are coming from the API, A lot of errors are coming and I am really having a difficult time dealing with this one since I am new to swift.
Structure:
{
"StatusCode": 0,
"Result": [
{
"Type": "Test",
"Date": "Test",
"Message": "Test"
},
{
"Type": "Test",
"Date": "Test",
"Message": "Test"
}
]
}
My Structure That is not Working:
struct Notifications: Decodable {
struct NotificationsStructure: Decodable {
var Type: String?
var Date: String?
var Message: String?
}
var StatusCode: Int
var Result: NotificationsStructure!
}
First of all never declare a property in a Decodable object as implicit unwrapped optional. If it can be missing or be nil declare it as regular optional (?) otherwise non-optional.
In JSON there are only two collection types:
dictionary represented by {},
array represented by []
The value for key Result is clearly an array.
And add CodingKeys to map the capitalized keys to lowercase property names
struct Notifications: Decodable {
struct NotificationsStructure: Decodable {
private enum CodingKeys: String, CodingKey {
case type = "Type", date = "Date", message = "Message"
}
let type: String
let date: String
let message: String
}
private enum CodingKeys: String, CodingKey {
case statusCode = "StatusCode", result = "Result",
}
let statusCode: Int
let result: [NotificationsStructure]
}
The response from result is an array type . You should create another object that have same properties of each array.
struct NotificationResponse : Decodable {
let StatusCode : Int
let Result: [Notification]
}
struct Notification : Decodable {
let Type: String
let Date: String
let Message: String
}

Getting key from Dictionary into neasted Decodable model

Let's say we have a JSON like that:
{
"id1": {
"name": "hello"
},
"id2": {
"name": "world"
}
}
A model:
struct Model: Decodable {
var id: String
var name: String
}
How is it possible to make an array of Model from the JSON above?
You could do something like this
let data = """
{
"id1": {
"name": "hello"
},
"id2": {
"name": "world"
}
}
""".data(using: .utf8)!
struct Name: Decodable {
let name: String
}
struct Model {
let id: String
let name: String
}
do {
let json = try JSONDecoder().decode([String: Name].self, from: data)
let result = json.map { Model(id: $0.key, name: $0.value.name) }
print(result)
} catch {
print(error)
}
We decode the data as [String, Name]. We could decode it as [String: [String:String]] but this will mean that we will have to handle optional values so it is easier to create a Name struct to handle that part.
Once we have the dictionary we map over it converting it into the model object, leaving an array of [Model]

Swift Decoding JSON from Amadeus

I have a problem with decoding JSON from API.
I get JSON:
{
"meta": {
"count": 1,
"links": {
"self": "https://test.api.amadeus.com/v1/reference-data/locations?subType=AIRPORT&keyword=barcel&sort=analytics.travelers.score&view=LIGHT&page%5Boffset%5D=0&page%5Blimit%5D=10"
}
},
"data": [
{
"type": "location",
"subType": "AIRPORT",
"name": "AIRPORT",
"detailedName": "BARCELONA/ES:AIRPORT",
"id": "ABCN",
"self": {
"href": "https://test.api.amadeus.com/v1/reference-data/locations/ABCN",
"methods": [
"GET"
]
},
"iataCode": "BCN",
"address": {
"cityName": "BARCELONA",
"countryName": "SPAIN"
}
}
]
}
My code looks like this:
struct Airport: Decodable {
let meta: MetaAirport
let data: AirportInfo
}
struct Address: Decodable{
let countryName: String
let cityName: String
}
struct AirportInfo: Decodable{
let address: Address
let subType: String
let id: String
let type: String
let detailedName: String
let name: String
let iataCode: String
}
struct MetaAirport : Decodable{
let count: Int
let links: Links
}
struct Links : Decodable{
let info: String
private enum CodingKeys : String, CodingKey {
case info = "self"
}
}
I'm trying to decode json using JSONDecoder on an AirportInfo object
do {
let airport = try JSONDecoder().decode(Airport.self, from: data!)
print(airport.data.name)
}
catch let err {
print(err)
}
I get an error
typeMismatch(Swift.Dictionary, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "data", intValue: nil)], debugDescription: "Expected to decode Dictionary but found an array instead.", underlyingError: nil))
What am I doing wrong?
In json data, address is a object. Change your code:
let address: [Address] -> let address: Address
Your Model should be like that to parse
struct DataModel: Codable {
let meta: Meta?
let data: [Datum]?
}
struct Datum: Codable {
let type, subType, name, detailedName: String?
let id: String?
let datumSelf: SelfClass?
let iataCode: String?
let address: Address?
enum CodingKeys: String, CodingKey {
case type, subType, name, detailedName, id
case datumSelf = "self"
case iataCode, address
}
}
struct Address: Codable {
let cityName, countryName: String?
}
struct SelfClass: Codable {
let href: String?
let methods: [String]?
}
struct Meta: Codable {
let count: Int?
let links: Links?
}
struct Links: Codable {
let linksSelf: String?
enum CodingKeys: String, CodingKey {
case linksSelf = "self"
}
}
I see you have edited your question from the original, so it feels hard to correctly identify at a given moment in time where the issue is.
However, Codable is great and one easy way for you to work through it is by constructing your object little by little. There is no requirement for an object to identically represent your JSON IF you make things optional. So when debugging this kind of issues, make different properties optional and see what got parsed. This would allow you to identify little by little where you're doing things wrong, without causing crashes and keeping the correctly formatted parts.
Also, pro tip: If you plan to keep this code for anything more than 1 week, stop using force unwrapping ! and get used to handle optionals correctly. Otherwise this is a hard habit to stop and others will hate the code you've created ;)

Deserialize JSON array based on nested type attribute

Consider this example JSON:
{
"sections": [{
"title": "Sign up",
"rows": [
{
"type": "image",
"imageURL": "https://example.com/image.jpg"
},
{
"type": "textField",
"value": "",
"placeholder": "Username"
},
{
"type": "textField",
"placeholder": "password"
},
{
"type": "textField",
"placeholder": "confirmPassword"
},
{
"type": "button",
"placeholder": "Register!"
}
]
}]
}
Let's say I wanted to parse the JSON above into the following models (I know it doesn't compile due to the Row protocol not corresponding to Decodable):
enum RowType: String, Codable {
case textField
case image
case button
}
protocol Row: Codable {
var type: RowType { get }
}
struct TextFieldRow: Row {
let type: RowType
let placeholder: String
let value: String
enum CodingKey: String {
case type
case placeholder
case value
}
}
struct ImageRow: Row {
let type: RowType
let imageURL: URL
enum CodingKey: String {
case type
case imageURL
}
}
struct ButtonRow: Row {
let type: RowType
let title: String
enum CodingKey: String {
case type
case title
}
}
struct Section: Codable {
let rows: [Row]
let title: String
enum CodingKey: String {
case rows
case title
}
}
struct Response: Codable {
let sections: [Section]
enum CodingKey: String {
case sections
}
}
// Parsing the response using the Foundation JSONDecoder
let data: Data // From network
let decoder = JSONDecoder()
do {
let response = try decoder.decode(Response.self, from: data)
} catch {
print("error: \(error)")
}
Is there a way to make the Swift code above Codable compliant?
I know you can manually solve this by first grabbing each Row's type string and then creating the right type of Row model as well as changing them from structs to classes and letting the Row protocol be a superclass instead. But is there a way that requires less manual labour?
Using an enum with associated value is the best option:
Consider this enum:
enum Row: Decodable {
case textField(TextFieldRow)
case image(ImageRow)
// and other cases
case unknown
enum CodingKeys: String, CodingKey {
case type
}
public init(from decoder: Decoder) throws {
do {
let selfContainer = try decoder.singleValueContainer()
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
let type = try typeContainer.decode(String.self, forKey: .type)
switch type {
case "textField": self = .textField( try selfContainer.decode(TextFieldRow.self) )
case "Image": self = .image( try selfContainer.decode(ImageRow.self) )
// and other cases
default: self = .unknown
}
}
}
}
With these changes:
struct TextFieldRow: Decodable {
let placeholder: String?
let value: String?
}
struct ImageRow: Decodable {
let imageURL: URL
}
// and so on
Now this will decode like a charm:
// Minmal testing JSON
let json = """
[
{
"type": "image",
"imageURL": "https://example.com/image.jpg"
},
{
"type": "textField",
"value": "",
"placeholder": "Username"
},
{
"type": "textField",
"placeholder": "password"
}
]
""".data(using: .utf8)!
let decoder = JSONDecoder()
print( try! decoder.decode([Row].self, from: json) )
You can now add any other case you need to the decoder to build your application builder app.

Cannot convert value of type '[String : Any]' to expected argument type 'Data' Alamofire - Codable

I'm new to programming so apologies if the fix is a simple one. I'm trying to get the JSON data from the Alamofire request to show up not as an optional in the console.
I've already tried response.data which does give me the data as an optional but I don't know how to unwrap that optional in this call. I've searched and have seen that result.value might be closer to what I need. Below is what I have so far. This results in a "Cannot convert value of type '[String : Any]' to expected argument type 'Data'" error.
JSON File-->
"forecasts": [
{
"dateLabel": "今日",
"telop": "晴時々曇",
"date": "2019-08-16",
"temperature": {
"min": null,
"max": null
},
"image": {
"width": 50,
"url": "http://weather.livedoor.com/img/icon/2.gif",
"title": "晴時々曇",
"height": 31
}
},
{
"dateLabel": "明日",
"telop": "晴時々曇",
"date": "2019-08-17",
"temperature": {
"min": {
"celsius": "27",
"fahrenheit": "80.6"
},
"max": {
"celsius": "37",
"fahrenheit": "98.6"
}
},
"image": {
"width": 50,
"url": "http://weather.livedoor.com/img/icon/2.gif",
"title": "晴時々曇",
"height": 31
}
},
{
"dateLabel": "明後日",
"telop": "晴時々曇",
"date": "2019-08-18",
"temperature": {
"min": null,
"max": null
},
"image": {
"width": 50,
"url": "http://weather.livedoor.com/img/icon/2.gif",
"title": "晴時々曇",
"height": 31
}
}
],
"location": {
"city": "東京",
"area": "関東",
"prefecture": "東京都"
},
"publicTime": "2019-08-16T17:00:00+0900",
"copyright": {
"provider": [
{
"link": "http://tenki.jp/",
"name": "日本気象協会"
}
],
"link": "http://weather.livedoor.com/",
"title": "(C) LINE Corporation",
"image": {
"width": 118,
"link": "http://weather.livedoor.com/",
"url": "http://weather.livedoor.com/img/cmn/livedoor.gif",
"title": "livedoor 天気情報",
"height": 26
}
}
Data model-->
import Foundation
import Alamofire
// MARK: - WeatherData
struct WeatherData: Codable {
let forecasts: [Forecast]
}
struct Forecast: Codable {
let dateLabel, telop, date: String
let temperature: Temperature
let image: Image
enum CodingKeys: String, CodingKey {
case dateLabel = "dateLabel"
case telop = "telop"
case date = "date"
case temperature
case image
}
}
struct Image: Codable {
let width: Int
let url: String
let title: String
let height: Int
enum CodingKeys: String, CodingKey {
case width = "width"
case url = "url"
case title = "title"
case height = "height"
}
}
struct Temperature: Codable {
let min, max: Max?
enum CodingKeys: String, CodingKey {
case min = "min"
case max = "max"
}
}
struct Max: Codable {
let celsius, fahrenheit: String
enum CodingKeys: String, CodingKey {
case celsius = "celsius"
case fahrenheit = "fahrenheit"
}
}
viewcontroller-->
import UIKit
import Alamofire
class ForecastTableViewController: UITableViewController {
let WEATHER_URL = "http://weather.livedoor.com/forecast/webservice/json/v1?city=130010"
override func viewDidLoad() {
super.viewDidLoad()
Alamofire.request(WEATHER_URL).responseJSON { (response) in
if let data = response.result.value as? [String: Any]{
let decoder = JSONDecoder()
let forecast = try? decoder.decode(WeatherData.self, from: data)
print(forecast?.forecasts)
}
}
}
My ultimate goal is to print out the JSON data into a tableview, including the images and dates. I think being able to unwrap this optional is the first step before I figure out the next part.
I've already tried response.data which does give me the data as an optional but I don't know how to unwrap that optional in this call.
You definitely should learn how to unwrap optionals properly. It basically comes down to what do you want to do when the value is nil. response.data could be nil when somehow the data could not be fetched, there's no internet, the server doesn't respond, or whatever reason it might be.
Think about what you want to happen in such a situation. Do you want to show an error as an alert to the user? Do you want to try again, or just do nothing?
And then use this code:
Alamofire.request(WEATHER_URL).responseData { (response) in
guard let data = response.data else {
// do the stuff you want to do when the data failed to be fetched
return
}
let decoder = JSONDecoder()
guard let forecast = try? decoder.decode(WeatherData.self, from: data) else {
// do the stuff you want to do when the data is not in the right format
return
}
print(forecast?.forecasts)
}
If you want to access the raw Data returned from a request, you need to use responseData. responseJSON parses the response using JSONSerialization and gives you an Any value. responseData gives you the raw Data returned, so you can unwrap it as you're currently doing and use the JSONDecoder.
You can also update to Alamofire 5 (currently in beta) and use responseDecodable to be able to parse Decodable types automatically.