Decode json string to struct from response data in Swift - json

I have a test server written in python which produces json output on single path: '/'
import json
from fastapi import FastAPI
from uvicorn import run
app = FastAPI()
#app.get('/')
def main():
return json.dumps([{
'name': 'ihor',
'age': 5
}])
if __name__ == '__main__':
run(app, host='0.0.0.0')
Then I have my iOS client in SwiftUI and trying to fetch a json from my server and decode it into struct.
Here's the code of api call and decoding:
import SwiftUI
class Network: ObservableObject {
#Published var users: [User] = []
func getUsers() {
let url = URL(string: "http://localhost:8000")
let urlRequest = URLRequest(url: url!)
let dataTask = URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
if let error = error {
print("Request error: ", error)
return
}
guard let response = response as? HTTPURLResponse else { return }
if response.statusCode == 200 {
guard let data = data else { return }
DispatchQueue.main.async {
do {
print(String(decoding: data, as: UTF8.self))
let decodedUsers = try JSONDecoder().decode([User].self, from: data)
self.users = decodedUsers
} catch let error {
print("Error decoding: ", error)
}
}
}
}
dataTask.resume()
}
}
struct User: Decodable {
var name: String
var age: Int
}
And it's called from simple content view
import SwiftUI
struct ContentView: View {
#EnvironmentObject var network: Network
var body: some View {
ScrollView {
Text("All users")
.font(.title).bold()
if network.users.count > 0 {
Text(network.users.first!.name)
}
}
.onAppear {
network.getUsers()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView() .environmentObject(Network())
}
}
After all it's produces the next output:
"[{\"name\": \"ihor\", \"age\": 5}]" Error decoding: typeMismatch(Swift.Array<Any>, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Array<Any> but found a string/data instead.", underlyingError: nil))
Looks like JSONDecoder cannot handle input with escaped backslashes and quotes at the start and at the end.
Everything works fine once I remove first and last quotes and escaped backslashes:
do {
let rawJsonString = String(decoding: data, as: UTF8.self)
let lowerIndex = rawJsonString.index(rawJsonString.startIndex, offsetBy: 1)
let upperIndex = rawJsonString.index(rawJsonString.endIndex, offsetBy: -1)
let jsonString = rawJsonString[lowerIndex..<upperIndex].replacingOccurrences(of: "\\", with: "")
print(jsonString)
let decodedUsers = try JSONDecoder().decode([User].self, from: jsonString.data(using:.utf8)!)
self.users = decodedUsers
} catch let error {
print("Error decoding: ", error)
}
But it's pretty dirty solution and wouldn't work with nested json that have to be escaped with backslashes.
Is there a simple solution to parse JSON like this to struct?

Related

Decoding json data from back4app API

I am having issues reading data from my back4app API into my swift project.I have connected my Back4app app to my swift project.Imported the necessary pods.
Below is how I connected my app to my swift project
import UIKit
import CoreData
import Parse
#main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
let configuration = ParseClientConfiguration {
$0.applicationId = "KwLoFN7Rik2qadAS6dSpnNIv5E0Qd1Uu0nhvO1dt"
$0.clientKey = "RVNjTMvt5l73jt5GQxsg9GI16CDOiPAbEXiVPTY1"
$0.server = "https://parseapi.back4app.com"
}
Parse.initialize(with: configuration)
return true
}
This is the data model I am using to decode my json data.My json data follows this structure
import UIKit
struct restaurant: Decodable{
let objectId: String
let restaurant: String
let name: String
let description: String
let calories: String
let price: String
let ImageUrl: String
let type: String
let createdAt: String
let updatedAt: String
}
struct restaurants: Decodable{
let restaurants : [restaurant]
}
In my viewdidload() method I check if my swift project has successfully connected to my back4app app and then I try retrieving the data using the url but I keep getting an error and I'm not sure what's it is
import UIKit
import Parse
class ViewController: UIViewController {
var restaurantStruct : restaurants? = nil
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
testParseConnection()
if let url = URL(string:"https://parseapi.back4app.com/classes/restaurants"){
let session = URLSession.shared
session.dataTask(with: url) { [self] (data, response, err) in
guard let jsonData = data else {
return
}
do {
let decoder = JSONDecoder()
let artworkList = try decoder.decode(restaurants.self, from: jsonData)
self.restaurantStruct = artworkList
DispatchQueue.main.async {
print(self.restaurantStruct ?? " ")
}
print(self.restaurantStruct!)
}
catch let jsonErr {
print("Error decoding JSON", jsonErr)
}
}
.resume()
}
func testParseConnection(){
let myObj = PFObject(className:"FirstClass")
myObj["message"] = "Hey ! First message from Swift. Parse is now connected"
myObj.saveInBackground { (success, error) in
if(success){
print("You are connected!")
}
else{
print("An error has occurred!")
}
}
}
}
}
This is the error that I see in my terminal
You are connected!
Error decoding JSON keyNotFound(CodingKeys(stringValue: "restaurants", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"restaurants\", intValue: nil) (\"restaurants\").", underlyingError: nil))
using parse to read data
var query = PFQuery(className:"SoccerPlayers")
query.getObjectInBackgroundWithId("HMcTr9rD3s") {
(soccerPlayers: PFObject?, error: NSError?) -> Void in
if error == nil && soccerPlayers != nil {
print(soccerPlayers)
} else {
print(error)
}
}
If you get an error like {"error":"unauthorized"}, you should use MasterKey but this dangerous for client side.
You can use sessionToken for authorized. But if you don`t know for now.
You can change CLP-Class level permissions from dashboard. You can open your app in back4app, and open the related Class. Do it public read and public write.

SwiftUI NavigationLink cannot find 'json' in scope

I'm new to SwiftUI and have worked through the server requests and JSON. I now need to programmatically transition to a new view which is where I get stuck with a "Cannot find 'json' in scope" error on the NavigationLink in ContentView.swift. I've watched videos and read articles but nothing quite matches, and everything I try just seems to make things worse.
JSON response from server
{"status":{"errno":0,"errstr":""},
"data":[
{"home_id":1,"name":"Dave's House","timezone":"Australia\/Brisbane"},
{"home_id":2,"name":"Mick's House","timezone":"Australia\/Perth"},
{"home_id":3,"name":"Jim's House","timezone":"Australia\/Melbourne"}
]}
JSON Struct file
import Foundation
struct JSONStructure: Codable {
struct Status: Codable {
let errno: Int
let errstr: String
}
struct Home: Codable, Identifiable {
var id = UUID()
let home_id: Int
let name: String
let timezone: String
}
let status: Status
let data: [Home]
}
ContentView file
import SwiftUI
struct ContentView: View {
#State private var PushViewAfterAction = false
var body: some View {
NavigationLink(destination: ListView(json: json.data), isActive: $PushViewAfterAction) {
EmptyView()
}.hidden()
Button(action: {
Task {
await performAnAction()
}
}, label: {
Text("TEST")
.padding()
.frame(maxWidth: .infinity)
.background(Color.blue.cornerRadius(10))
.foregroundColor(.white)
.font(.headline)
})
}
func performAnAction() {
PushViewAfterAction = true
return
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
ListView file
import SwiftUI
struct ListView: View {
#State var json: JSONStructure
var body: some View {
VStack {
List (self.json.data) { (home) in
HStack {
Text(home.name).bold()
Text(home.timezone)
}
}
}.onAppear(perform: {
guard let url: URL = URL(string: "https://... ***removed*** ") else {
print("invalid URL")
return
}
var urlRequest: URLRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
URLSession.shared.dataTask(with: urlRequest, completionHandler: { (data, response, error) in
// check if response is okay
guard let data = data, error == nil else { // check for fundamental networking error
print((error?.localizedDescription)!)
return
}
let httpResponse = (response as? HTTPURLResponse)!
if httpResponse.statusCode != 200 { // check for http errors
print("httpResponse Error: \(httpResponse.statusCode)")
return
}
// convert JSON response
do {
self.json = try JSONDecoder().decode(JSONStructure.self, from: data)
} catch {
print(error.localizedDescription)
print(String(data: data, encoding: String.Encoding.utf8)!)
}
print(json)
if (json.status.errno != 0) {
print(json.status.errstr)
}
print("1. \(json.data[0].name)), \(json.data[0].timezone)")
print("2. \(json.data[1].name)), \(json.data[1].timezone)")
}).resume()
})
}
}
struct ListView_Previews: PreviewProvider {
static var previews: some View {
ListView()
}
}
I've tried to keep the code to a minimum for clarity.
It's because there is no "json" in ContentView, you need to pass json object to ListView, but since you load json in ListView, then you need to initialize json in ListView like:
struct ListView: View {
#State var json: JSONStructure = JSONStructure(status: JSONStructure.Status(errno: 0, errstr: ""), data: [JSONStructure.Home(home_id: 0, name: "", timezone: "")])
var body: some View {
and remove it form NavigationLink in ContentView like:
NavigationLink(destination: ListView(), isActive: $PushViewAfterAction) {
or you could build your JSONStructure to accept optional like:
import Foundation
struct JSONStructure: Codable {
struct Status: Codable {
let errno: Int?
let errstr: String?
init() {
errno = nil
errstr = nil
}
}
struct Home: Codable, Identifiable {
var id = UUID()
let home_id: Int?
let name: String?
let timezone: String?
init() {
home_id = nil
name = nil
timezone = nil
}
}
let status: Status?
let data: [Home]
init() {
status = nil
data = []
}
}
but then you need to check for optionals or provide default value like:
struct ListView: View {
#State var json: JSONStructure = JSONStructure()
var body: some View {
VStack {
List (self.json.data) { (home) in
HStack {
Text(home.name ?? "Could not get name").bold()
Text(home.timezone ?? "Could not get timeZone")
}
}
}.onAppear(perform: {
guard let url: URL = URL(string: "https://... ***removed*** ") else {
print("invalid URL")
return
}
var urlRequest: URLRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
URLSession.shared.dataTask(with: urlRequest, completionHandler: { (data, response, error) in
// check if response is okay
guard let data = data, error == nil else { // check for fundamental networking error
print((error?.localizedDescription)!)
return
}
let httpResponse = (response as? HTTPURLResponse)!
if httpResponse.statusCode != 200 { // check for http errors
print("httpResponse Error: \(httpResponse.statusCode)")
return
}
// convert JSON response
do {
self.json = try JSONDecoder().decode(JSONStructure.self, from: data)
} catch {
print(error.localizedDescription)
print(String(data: data, encoding: String.Encoding.utf8)!)
}
print(json)
if (json.status?.errno != 0) {
print(json.status?.errstr)
}
print("1. \(json.data[0].name)), \(json.data[0].timezone)")
print("2. \(json.data[1].name)), \(json.data[1].timezone)")
}).resume()
})
}
}

SwiftUI List and URLSession + JSONDecode

hi everyone I'm newbie in Swift
I ask for help why the data is not displayed in List? Please help me a fix it.
I make a Model Data, and take a data from URl and parse it
My model:
struct ResponseData: Codable{
var userId: String
var id: String
var title: String
var body: String
}
My code in Content view:
import SwiftUI
struct ContentView: View {
#State var responseData = [ResponseData]()
var body: some View {
List(responseData, id: \.id ) { item in
Text("\(item.body)")
}
.task {
await loadData()
}
}
func loadData () async {
let urlString = "https://jsonplaceholder.typicode.com/posts"
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else {return}
guard error == nil else {return}
do {
let postData = try JSONDecoder().decode(ResponseData.self, from: data)
} catch let error {
print(error)
}
}.resume()
}
}
There are many issues in the code.
The most significant ones are
In the struct userId and id are Int.
The JSON root object is an array, please note the starting [.
You don't update the model after receiving the data.
As you are using async/await anyway take full advantage of the pattern.
The JSON API returns posts, so how about to name the struct Post?
struct Post: Codable, Identifiable {
let userId: Int
let id: Int
let title: String
let body: String
}
struct ContentView : View {
#State var posts = [Post]()
var body : some View {
List(posts) { post in
Text(post.body)
}
.task {
do {
posts = try await loadData()
} catch {
print(error)
}
}
}
func loadData() async throws -> [Post] {
let urlString = "https://jsonplaceholder.typicode.com/posts"
let url = URL(string: urlString)!
let (data, _) = try await URLSession.shared.data(for: URLRequest(url: url))
return try JSONDecoder().decode([Post].self, from: data)
}
}

Fetching Data from API in Swift

So I am trying to fetch data from the Pokemon API, and I am getting stuck at the point where I am trying to decode the JSON into a struct. Here is my code:
{
"count":1118,
"next":"https://pokeapi.co/api/v2/pokemon/?offset=20&limit=20",
"previous":null,
"results":
[
{"name":"bulbasaur","url":"https://pokeapi.co/api/v2/pokemon/1/"},
{"name":"ivysaur","url":"https://pokeapi.co/api/v2/pokemon/2/"},
{"name":"venusaur","url":"https://pokeapi.co/api/v2/pokemon/3/"},
{"name":"charmander","url":"https://pokeapi.co/api/v2/pokemon/4/"},
{"name":"charmeleon","url":"https://pokeapi.co/api/v2/pokemon/5/"},
{"name":"charizard","url":"https://pokeapi.co/api/v2/pokemon/6/"},
{"name":"squirtle","url":"https://pokeapi.co/api/v2/pokemon/7/"},
{"name":"wartortle","url":"https://pokeapi.co/api/v2/pokemon/8/"},
{"name":"blastoise","url":"https://pokeapi.co/api/v2/pokemon/9/"},
{"name":"caterpie","url":"https://pokeapi.co/api/v2/pokemon/10/"},
{"name":"metapod","url":"https://pokeapi.co/api/v2/pokemon/11/"},
{"name":"butterfree","url":"https://pokeapi.co/api/v2/pokemon/12/"},
{"name":"weedle","url":"https://pokeapi.co/api/v2/pokemon/13/"},
{"name":"kakuna","url":"https://pokeapi.co/api/v2/pokemon/14/"},
{"name":"beedrill","url":"https://pokeapi.co/api/v2/pokemon/15/"},
{"name":"pidgey","url":"https://pokeapi.co/api/v2/pokemon/16/"},
{"name":"pidgeotto","url":"https://pokeapi.co/api/v2/pokemon/17/"},
{"name":"pidgeot","url":"https://pokeapi.co/api/v2/pokemon/18/"},
{"name":"rattata","url":"https://pokeapi.co/api/v2/pokemon/19/"},
{"name":"raticate","url":"https://pokeapi.co/api/v2/pokemon/20/"}
]
}
func fetchPokemon() {
let defaultSession = URLSession(configuration: .default)
if let url = URL(string: "https://pokeapi.co/api/v2/pokemon/") {
let request = URLRequest(url:url)
let dataTask = defaultSession.dataTask(with: request, completionHandler: { (data, response, error) -> Void in
guard error == nil else {
print ("error: ", error!)
return
}
guard data != nil else {
print("No data object")
return
}
guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
print("response is: ", response!)
return
}
guard let mime = response?.mimeType, mime == "application/json" else {
print("Wrong MIME type!")
return
}
DispatchQueue.main.async {
guard let result = try? JSONDecoder().decode(PokemonList.self, from: data!) else {
print("Error Parsing JSON")
return
}
let pokemon = result.pokemon
self.Pokemon = pokemon
print(self.Pokemon)
}
})
dataTask.resume()
}
}
and here is the pokemon struct:
struct Pokemon {
// Various properties of a post that we either need or want to display
let name: String
let url: String
}
extension Pokemon: Decodable {
// properties within a Post returned from the Product Hunt API that we want to extract the info from.
enum PokemonKeys: String, CodingKey {
// first three match our variable names for our Post struct
case name = "name"
case url = "url"
}
init(from decoder: Decoder) throws {
let postsContainer = try decoder.container(keyedBy: PokemonKeys.self)
name = try postsContainer.decode(String.self, forKey: .name)
url = try postsContainer.decode(String.self, forKey: .url)
}
}
struct PokemonList: Decodable {
var pokemon: [Pokemon]
}
It keeps reaching the point when decoding which says "Error Parsing JSON". I'm assuming that there may be an error in how I setup the pokemon struct?
Any ideas?
you are getting a parse error because the data model is not the same. your struct should be:
struct PokemonList: Decodable {
var results: [Pokemon]
var count: Int
var next: String
}
you don't need the extension.

Decoding Exchange Rate JSON in SwiftUI

I am trying to decode https://api.exchangeratesapi.io/latest, provided by Exchange Rates API. I'm applying several tutorials I found online, but when I apply my own details, I get an error. My code looks as following:
struct Response: Codable {
var results: [Result]
}
struct Result: Codable {
let base: String
let date: String
let rates: [String:Double]
}
The function to retrieve the data:
func loadData() {
guard let url = URL(string: "https://api.exchangeratesapi.io/latest") else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let decodedResponse = try? JSONDecoder().decode(Response.self, from: data) {
// we have good data – go back to the main thread
DispatchQueue.main.async {
// update our UI
self.results = decodedResponse.results
}
// everything is good, so we can exit
return
}
}
// if we're still here it means there was a problem
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
}.resume()
}
And my view:
import SwiftUI
struct ExchangeRateTest: View {
#State private var results = [Result]()
var body: some View {
List(results, id: \.base) { item in
VStack(alignment: .leading) {
Text(item.base)
}
}.onAppear(perform: loadData)
}
}
The error I get is: Fetch Failed: Unknown Error, suggesting that the app is not able to read the online data. What can cause this?
It has nothing to do with my network connection; if I apply another JSON this approach works fine.
Any help would greatly be appreciated.
you can read like this:
struct RateResult: Codable {
let rates: [String: Double]
let base, date: String
}
struct ContentView: View {
#State private var results = RateResult(rates: [:], base: "", date: "")
func loadData() {
guard let url = URL(string: "https://api.exchangeratesapi.io/latest") else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let decodedResponse = try? JSONDecoder().decode(RateResult.self, from: data) {
// we have good data – go back to the main thread
DispatchQueue.main.async {
// update our UI
self.results = decodedResponse
}
// everything is good, so we can exit
return
}
}
// if we're still here it means there was a problem
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
}.resume()
}