Swift: Decode flat JSON to an structured object - json

wonder if there is a simple way to get a simple flat json to a struct with a structure
json:
{
"date": "2022-02-24T00:00:00.000Z",
"personel": 800,
"plane": 7,
"drone": 6,
}
what I want is to get a structure like this:
struct Day: Codable, Identifiable {
var id = UUID()
var date : String
var dayData: [DayData]
struct DayData: Codable {
var personel: Int
var plane: Int
var drone: Int
}
}
I thought there should be some simple way to make it work

As specified in comments it would be better to create temporary DBObject and initialize your Day object with it.
Simple DBObject:
struct DBObject: Decodable {
let date: String?
let personel, plane, drone: Int?
}
Adding init() to your Day
struct Day: Codable, Identifiable {
var id = UUID()
var date : String
var dayData: [DayData]
init(from dbObject: DBObject) {
let dayData = DayData(personel: dbObject.personel ?? 0,
plane: dbObject.plane ?? 0,
drone: dbObject.drone ?? 0)
self.dayData = [dayData]
self.date = dbObject.date ?? ""
}
struct DayData: Codable {
var personel: Int
var plane: Int
var drone: Int
}
}
Decoding Day from Data response:
func returnDay(from dataResponse: Data) -> Day {
do {
let decoder = JSONDecoder()
let dbObject = try decoder.decode(DBObject.self,
from: dataResponse)
return Day(from: dbObject)
} catch {
fatalError("Cannot decode object")
}
}

Related

How to read and display a dictionary from JSON?

I am working on an app that fetches the data from JSON and displays it.
However, I am stuck with an error saying Instance method 'appendInterpolation(_:formatter:)' requires that '[String : Int]' inherit from 'NSObject'
Here is my data structure:
struct Data: Codable {
var message: String
var data: Objects
}
struct Objects: Codable {
var date: String
var day: Int
var resource: String
var stats, increase: [String: Int]
}
Function to fetch the data:
func getData() {
let urlString = "https://russianwarship.rip/api/v1/statistics/latest"
let url = URL(string: urlString)
URLSession.shared.dataTask(with: url!) { data, _, error in
if let data = data {
do {
let decoder = JSONDecoder()
let decodedData = try decoder.decode(Data.self, from: data)
self.data = decodedData
} catch {
print("Hey there's an error: \(error.localizedDescription)")
}
}
}.resume()
}
And a ContentView with the #State property to pass the placeholder data:
struct ContentView: View {
#State var data = Data(message: "", data: Objects(date: "123", day: 123, resource: "", stats: ["123" : 1], increase: ["123" : 1]))
var body: some View {
VStack {
Button("refresh") { getData() }
Text("\(data.data.date)")
Text("\(data.data.day)")
Text(data.message)
Text("\(data.data.stats)") //error is here
Here is an example of JSON response
I wonder if the problem is in data structure, because both
Text("\(data.data.date)")
Text("\(data.data.day)")
are working just fine. If there are any workarounds with this issue – please, I would highly appreciate your help!:)
stats is [String: Int], and so when you want to use it, you need to supply the key to get the value Int, the result is an optional that you must unwrap or supply a default value in Text
So use this:
Text("\(data.data.stats["123"] ?? 0)")
And as mentioned in the comments, do not use Data for your struct name.
EDIT-1: there are two ways you can make the struct fields camelCase; one is using the CodingKeys as shown in ItemModel, or at the decoding stage, as shown in the getData() function. Note, I've also updated your models to make them easier to use.
struct DataModel: Codable {
var message: String
var data: ObjectModel
}
struct ObjectModel: Codable {
var date: String
var day: Int
var resource: String
var stats: ItemModel
var increase: ItemModel
}
struct ItemModel: Codable {
var personnelUnits: Int
var tanks: Int
var armouredFightingVehicles: Int
// ...
// manual CodingKeys
// enum CodingKeys: String, CodingKey {
// case tanks
// case personnelUnits = "personnel_units"
// case armouredFightingVehicles = "armoured_fighting_vehicles"
// }
}
struct ContentView: View {
#State var dataModel = DataModel(message: "", data: ObjectModel(date: "123", day: 123, resource: "", stats: ItemModel(personnelUnits: 123, tanks: 456, armouredFightingVehicles: 789), increase: ItemModel(personnelUnits: 3, tanks: 4, armouredFightingVehicles: 5)))
var body: some View {
VStack {
Button("get data from Server") { getData() }
Text("\(dataModel.data.date)")
Text("\(dataModel.data.day)")
Text(dataModel.message)
Text("\(dataModel.data.stats.armouredFightingVehicles)") // <-- here
}
}
func getData() {
let urlString = "https://russianwarship.rip/api/v1/statistics/latest"
if let url = URL(string: urlString) {
URLSession.shared.dataTask(with: url) { data, _, error in
if let data = data {
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase // <-- here
dataModel = try decoder.decode(DataModel.self, from: data)
} catch {
print("--> error: \(error)")
}
}
}.resume()
}
}
}

JSON decoder The data couldn’t be read because it isn’t in the correct format

I am new to this. Somehow I am able to understand how to do this.
I am doing below, but it's giving error- The data couldn’t be read because it isn’t in the correct format.Can someone help me with this? I am stuck on this from past 4 days. I really appreciate.
import SwiftUI
import Foundation
import Combine
struct Movie: Decodable, Identifiable {
var id: Int
var video: String
var vote_count: String
var vote_average: String
var title: String
var release_date: String
var original_language: String
var original_title: String
}
struct MovieList: Decodable{
var results: [Movie]
___________
class NetworkingManager : ObservableObject{
var objectWillChange = PassthroughSubject<NetworkingManager, Never>()
#Published var movies = [Movie]()
init() {
load()
}
func load(){
let url = URL(string: "https://api.themoviedb.org/3/discover/movie?sort_by=popularity.desc&api_key=<HIDDEN>")!
URLSession.shared.dataTask(with: url){ (data, response, error) in
do {
if let d = data {
let decodedLists = try JSONDecoder().decode([Movie].self, from: d)
DispatchQueue.main.async {
self.movies = decodedLists
}
}else {
print("No Data")
}
} catch {
print (error.localizedDescription)
}
}.resume()
}
}
This is how the response looks like:
{
"page": 1,
"results": [
{
"id": 419704,
"video": false,
"vote_count": 1141,
"vote_average": 6.2,
"title": "Ad Astra",
"release_date": "2019-09-17",
"original_language": "en",
"original_title": "Ad Astra",
"genre_ids": [
878
],
"backdrop_path": "/5BwqwxMEjeFtdknRV792Svo0K1v.jpg",
"adult": false,
"overview": "An astronaut travels to the outer edges of the solar system to find his father and unravel a mystery that threatens the survival of Earth. In doing so, he uncovers secrets which challenge the nature of human existence and our place in the cosmos.",
"poster_path": "/xJUILftRf6TJxloOgrilOTJfeOn.jpg",
"popularity": 227.167,
"media_type": "movie"
},
]
}
Code should fetch the data and hold in it the array I created. So that I can use it to display in the front end.
I had to consume the exact same API for a similar project and this is how I did it.
When calling:
let response = try JSONDecoder().decode(MovieResponse.self, from: data)
It needs to match the same properties that the JSON response returns.
Below you'll see a MovieResponse struct and the Movie class, which will list all of the properties and return types that the JSON response returns.
The type adopts Codable so that it's decodable using a JSONDecoder instance.
See this official example for more information regarding Codable.
A type that can convert itself into and out of an external representation.
Provided they match then the JSONDecoder() will work to decode the data.
ContentView.swift:
struct ContentView: View {
#EnvironmentObject var movieViewModel: MovieListViewModel
var body: some View {
MovieList(movie: self.movieViewModel.movie)
}
}
MovieListViewModel.swift:
public class MovieListViewModel: ObservableObject {
public let objectWillChange = PassthroughSubject<MovieListViewModel, Never>()
private var movieResults: [Movie] = []
var movie: MovieResults = [Movie]() {
didSet {
objectWillChange.send(self)
}
}
func load(url: String = "https://api.themoviedb.org/3/discover/movie?sort_by=popularity.desc&api_key=<HIDDEN>") {
guard let url = URL(string: url) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
do {
guard let data = data else { return }
let response = try JSONDecoder().decode(MovieResponse.self, from: data)
DispatchQueue.main.async {
for movie in response.results {
self.movieResults.append(movie)
}
self.movie = self.movieResults
print("Finished loading Movies")
}
} catch {
print("Failed to decode: ", error)
}
}.resume()
}
}
MovieResponse.swift:
struct MovieResponse: Codable {
var page: Int
var total_results: Int
var total_pages: Int
var results: [Movie]
}
public class Movie: Codable, Identifiable {
public var popularity: Float
public var vote_count: Int
public var video: Bool
public var poster_path: String
public var id: Int
public var adult: Bool
public var backdrop_path: String
public var original_language: String
public var original_title: String
public var genre_ids: [Int]
public var title: String
public var vote_average: Float
public var overview: String
public var release_date: String
enum CodingKeys: String, CodingKey {
case popularity = "popularity"
case vote_count = "vote_count"
case video = "video"
case poster_path = "poster_path"
case id = "id"
case adult = "adult"
case backdrop_path = "backdrop_path"
case original_language = "original_language"
case original_title = "original_title"
case genre_ids = "genre_ids"
case title = "title"
case vote_average = "vote_average"
case overview = "overview"
case release_date = "release_date"
}
public init(popularity: Float, vote_count: Int, video: Bool, poster_path: String, id: Int, adult: Bool, backdrop_path: String, original_language: String, original_title: String, genre_ids: [Int], title: String, vote_average: Float, overview: String, release_date: String) {
self.popularity = popularity
self.vote_count = vote_count
self.video = video
self.poster_path = poster_path
self.id = id
self.adult = adult
self.backdrop_path = backdrop_path
self.original_language = original_language
self.original_title = original_title
self.genre_ids = genre_ids
self.title = title
self.vote_average = vote_average
self.overview = overview
self.release_date = release_date
}
public init() {
self.popularity = 0.0
self.vote_count = 0
self.video = false
self.poster_path = ""
self.id = 0
self.adult = false
self.backdrop_path = ""
self.original_language = ""
self.original_title = ""
self.genre_ids = []
self.title = ""
self.vote_average = 0.0
self.overview = ""
self.release_date = ""
}
}
public typealias MovieResults = [Movie]
MovieCellViewModel.swift:
public class MovieCellViewModel {
private var movie: Movie
public init(movie: Movie) {
self.movie = movie
}
public func getTitle() -> String {
return self.movie.title
}
// add more properties or functions here
}
MovieCell.swift:
struct MovieCell: View {
var movieCellViewModel: MovieCellViewModel
var body: some View {
Text(self.movieCellViewModel.getTitle())
}
}
MovieList.swift:
struct MovieList: View {
#EnvironmentObject var movieViewModel: MovieListViewModel
var movie: MovieResults
var body: some View {
List(self.movie) { movie in
MovieCell(movieCellViewModel: MovieCellViewModel(movie: movie))
}
}
}
I had a same error, but for the properties inside the struct for JSON object doesn't match JSON file. I fixed this error by setting the property names same as JSON file properties, and in the same order.
Swift file:
struct Coin: Decodable {
var asset_id_base: String
var rates: [CoinDetail]
}
struct CoinDetail: Decodable {
var time: String
var asset_id_quote: String
var rate: Double
}
JSON File:
{
"asset_id_base": "BTC",
"rates": [
{
"time": "2020-11-08T07:50:02.2865270Z",
"asset_id_quote": "CZK",
"rate": 328886.3419989546
},
{
"time": "2020-11-08T07:51:15.0750421Z",
"asset_id_quote": "GPL2",
"rate": 92123.4454168586
},
The Movie struct doesn't match the API response. You missed at least top level, like page and results array. Here you can paste your JSON answer and get the needed struct:
// MARK: - APIAnswer
struct APIAnswer: Codable {
let page: Int
let results: [Result]
}
// MARK: - Result
struct Result: Codable {
let id: Int
let video: Bool
let voteCount: Int
let voteAverage: Double
let title, releaseDate, originalLanguage, originalTitle: String
let genreIDS: [Int]
let backdropPath: String
let adult: Bool
let overview, posterPath: String
let popularity: Double
let mediaType: String
enum CodingKeys: String, CodingKey {
case id, video
case voteCount = "vote_count"
case voteAverage = "vote_average"
case title
case releaseDate = "release_date"
case originalLanguage = "original_language"
case originalTitle = "original_title"
case genreIDS = "genre_ids"
case backdropPath = "backdrop_path"
case adult, overview
case posterPath = "poster_path"
case popularity
case mediaType = "media_type"
}
}
Then you need to use the top level struct, like:
// ...
let decodeResult = try JSONDecoder().decode([APIAnswer].self, from: d)
let movieList = decodeResult.results
// the other advice: don't just take answer and put it to the array.
// API can have errors too, so you can get the array with 2 equal id, for example
update checked your code for second time: you use
let decodeResult = try JSONDecoder().decode([Movie].self, from: d)
instead of:
let decodeResult = try JSONDecoder().decode(MovieList.self, from: d)
P.S. better to attach also the full error from Xcode in future

How to decode a JSON with array root in Swift

I'm trying to parse some data i got from an api request. The problem is, none of the values have labels. i want to add labels to each value so i can reference the labels later in program.
struct dataSet : Codable {
var variable1 : Int
var variable2 : Double
var variable3 : Double
var variable4 : Double
var variable5 : Double
var variable6 : Double
var variable7 : Double
var variable8 : Int
}
struct firsBatch : Codable {
var dataSet : [dataSet]
}
struct results : Codable {
var firsBatch : firsBatch
var last : Int
}
struct allData : Codable {
var errors : [String]
var results : results
}
//some api request code {...}
do {
let decoder = JSONDecoder()
let parsedJSON = try decoder.decode(allData.self, from: data!)
print(parsedJSON)
} catch {
print("JSON error: \(error.localizedDescription)")
}
//Sample of data
{"error":[],"results":{"firsBatch":[
[21,"93423.5","324.5","21.0","63.0","1253.0","12.34",1],[42,"314.0","431.1","2341.0","67.1","6567.0","0.8754",4],[12,"4312.1","12.1","43.1","3432.1","0.0","123.432",0],[422,"23442.1","12.1","654.1","12.1","723.1","23.34521",1]
],"last":64274}}
You have an array of Variables, not just a single Variables instance. So your decoding type should be [Variables].self, not Variables.self:
let parsedJSON = try decoder.decode(variables.self, from: data!)

How to customize raw value in an enum

What is my json looks like :
{
"2019-08-27 19:00:00": {
"temperature": {
"sol":292
}
}
,
"2019-08-28 19:00:00": {
"temperature": {
"sol":500
}
}
}
Here is a method to get the current next five days in the format needed :
func getFormatedDates() -> [String] {
let date = Date()
let format = DateFormatter()
format.dateFormat = "yyyy-MM-dd"
var dateComponents = DateComponents()
var dates = [String]()
for i in 0...4 {
dateComponents.setValue(i, for: .day)
guard let nextDay = Calendar.current.date(byAdding: dateComponents, to: date) else { return [""] }
let formattedDate = format.string(from: nextDay)
dates.append(formattedDate + " " + "19:00:00")
}
return dates
}
Because date key in the API constantly changes, I need dynamic keys. I would like to use this method inside an enum like in my Model :
var dates = getFormatedDates()
let firstForcast: FirstForcast
let secondForcast: SecondForcast
enum CodingKeys: String, CodingKey {
case firstForcast = dates[0]
case secondForcast = dates[1]
}
Any ideas ?
Create related types as below,
// MARK: - PostBodyValue
struct PostBodyValue: Codable {
let temperature: Temperature
}
// MARK: - Temperature
struct Temperature: Codable {
let sol: Int
}
typealias PostBody = [String: PostBodyValue]
and decode like this,
do {
let data = // Data from the API
let objects = try JSONDecoder().decode(PostBody.self, from: data)
for(key, value) in objects {
print(key)
print(value.temperature.sol)
}
} catch {
print(error)
}

Swift 4 JSON Codable ids as keys

I would like to use Swift 4's codable feature with json but some of the keys do not have a set name. Rather there is an array and they are ids like
{
"status": "ok",
"messages": {
"generalMessages": [],
"recordMessages": []
},
"foundRows": 2515989,
"data": {
"181": {
"animalID": "181",
"animalName": "Sophie",
"animalBreed": "Domestic Short Hair / Domestic Short Hair / Mixed (short coat)",
"animalGeneralAge": "Adult",
"animalSex": "Female",
"animalPrimaryBreed": "Domestic Short Hair",
"animalUpdatedDate": "6/26/2015 2:00 PM",
"animalOrgID": "12",
"animalLocationDistance": ""
where you see the 181 ids. Does anyone know how to handle the 181 so I can specify it as a key? The number can be any number and is different for each one.
Would like something like this
struct messages: Codable {
var generalMessages: [String]
var recordMessages: [String]
}
struct data: Codable {
var
}
struct Cat: Codable {
var Status: String
var messages: messages
var foundRows: Int
//var 181: [data] //What do I place here
}
Thanks in advance.
Please check :
struct ResponseData: Codable {
struct Inner : Codable {
var animalID : String
var animalName : String
private enum CodingKeys : String, CodingKey {
case animalID = "animalID"
case animalName = "animalName"
}
}
var Status: String
var foundRows: Int
var data : [String: Inner]
private enum CodingKeys: String, CodingKey {
case Status = "status"
case foundRows = "foundRows"
case data = "data"
}
}
let json = """
{
"status": "ok",
"messages": {
"generalMessages": ["dsfsdf"],
"recordMessages": ["sdfsdf"]
},
"foundRows": 2515989,
"data": {
"181": {
"animalID": "181",
"animalName": "Sophie"
},
"182": {
"animalID": "182",
"animalName": "Sophie"
}
}
}
"""
let data = json.data(using: .utf8)!
let decoder = JSONDecoder()
do {
let jsonData = try decoder.decode(ResponseData.self, from: data)
for (key, value) in jsonData.data {
print(key)
print(value.animalID)
print(value.animalName)
}
}
catch {
print("error:\(error)")
}
I don't think you can declare a number as a variable name. From Apple's doc:
Constant and variable names can’t contain whitespace characters,
mathematical symbols, arrows, private-use (or invalid) Unicode code
points, or line- and box-drawing characters. Nor can they begin with a
number, although numbers may be included elsewhere within the name.
A proper way is to have a property capturing your key.
struct Cat: Codable {
var Status: String
var messages: messages
var foundRows: Int
var key: Int // your key, e.g., 181
}
I would suggest something like this:-
struct ResponseData: Codable {
struct AnimalData: Codable {
var animalId: String
var animalName: String
private enum CodingKeys: String, CodingKey {
case animalId = "animalID"
case animalName = "animalName"
}
}
var status: String
var foundRows: Int
var data: [AnimalData]
private enum CodingKeys: String, CodingKey {
case status = "status"
case foundRows = "foundRows"
case data = "data"
}
struct AnimalIdKey: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let animalsData = try container.nestedContainer(keyedBy: AnimalIdKey.self, forKey: .data)
self.foundRows = try container.decode(Int.self, forKey: .foundRows)
self.status = try container.decode(String.self, forKey: .status)
self.data = []
for key in animalsData.allKeys {
print(key)
let animalData = try animalsData.decode(AnimalData.self, forKey: key)
self.data.append(animalData)
}
}
}
let string = "{\"status\":\"ok\",\"messages\":{\"generalMessages\":[],\"recordMessages\":[]},\"foundRows\":2515989,\"data\":{\"181\":{\"animalID\":\"181\",\"animalName\":\"Sophie\"}}}"
let jsonData = string.data(using: .utf8)!
let decoder = JSONDecoder()
let parsedData = try? decoder.decode(ResponseData.self, from: jsonData)
This way your decoder initializer itself handles the keys.