parsing json with dynamic keys - json

I have been working on this for a few hours and have not been able to find an answer. I have a JSON with dynamic keys that I am trying to parse into a struct. I thought I could keep it simple but I'm getting serialization errors. Please help - thanks
{"rates":{
"btc":{"name":"Bitcoin","unit":"BTC","value":1.0,"type":"crypto"},
"eth":{"name":"Ether","unit":"ETH","value":35.69,"type":"crypto"},
}}
my stuct
struct CryptoCoins: Decodable {
let rates: [String: [Coin]]
}
struct Coin: Decodable {
let name: String
let unit: String
let value: Double
let type: String
}
my decoder:
guard let container = try? JSONDecoder().decode(CryptoCoins.self, from: json) else {
completion(.failure(.serializationError)) // <- failing here
return
}

You're decoding the property rates into the wrong type - it's not a dictionary of String keys and an array of Coin values - it's just a single Coin value.
struct CryptoCoins: Decodable {
let rates: [String: Coin] // <- here
}
On a related note, don't hide the error with try?. Capture it and log it, if necessary:
do {
let cryptoCoins = try JSONDecoder().decode(CryptoCoins.self, from: json)
// ..
} catch {
print(error)
}
Then you would have gotten a typeMismatch error for btc key: "Expected to decode Array<Any> but found a dictionary instead.", which would have at least given you a hint of where to look.

Related

Parse JSON as array of objects instead of dictionary using Codable in Swift

I receive the following JSON from API
{
"someProperty":"someValue",
"type":"type1",
"values":{
"first":"someValue",
"second":"someValue" // keys and values are always different !
}
}
I created model to parse this JSON and it works.
struct MyModel: Codable {
let someProperty: String
let type: SomeType
var values: [String: String]?
}
enum SomeType: Codable {
case type1, type2
}
But I need to get array of objects instead of dictionary. Because I need to get values in right order.
I want something like this:
struct MyModel: Codable {
let someProperty: String
let type: SomeType
var values: [Value]?
}
enum SomeType: Codable {
case type1, type2
}
struct Value: Codable {
let key: String
let value: String
}
And I don't understand what should I change in my code to make this model works. Because now I get error "Expected to decode Array but found a dictionary instead."
Is there a way to parse JSON like I want?
Any suggestions? Maybe example of code with similar parsing
Thanks for your time and your help!
Create a custom init. In it decode the dictionary and map it into an array of your Value:
struct MyModel: Codable {
let someProperty: String
let type: SomeType
var values: [Value]?
enum CodingKeys: CodingKey{
case someProperty, type, values
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
someProperty = try container.decode(String.self, forKey: .someProperty)
type = try container.decode(SomeType.self, forKey: .type)
let dictionary = try container.decodeIfPresent([String:String].self, forKey: .values)
values = dictionary?.map{Value(key: $0.key, value: $0.value)}
}
}

Parse JSON from file using Codable swift

I have JSON file with cities
[
{"country":"UA","name":"Hurzuf","_id":707860,"coord":{"lon":34.283333,"lat":44.549999}},
{"country":"RU","name":"Novinki","_id":519188,"coord":{"lon":37.666668,"lat":55.683334}},
{"country":"NP","name":"Gorkhā","_id":1283378,"coord":{"lon":84.633331,"lat":28}},
{"country":"IN","name":"State of Haryāna","_id":1270260,"coord":{"lon":76,"lat":29}},
{"country":"UA","name":"Holubynka","_id":708546,"coord":{"lon":33.900002,"lat":44.599998}},
{"country":"NP","name":"Bāgmatī Zone","_id":1283710,"coord":{"lon":85.416664,"lat":28}},
{"country":"RU","name":"Mar’ina Roshcha","_id":529334,"coord":{"lon":37.611111,"lat":55.796391}},
{"country":"IN","name":"Republic of India","_id":1269750,"coord":{"lon":77,"lat":20}},
{"country":"NP","name":"Kathmandu","_id":1283240,"coord":{"lon":85.316666,"lat":27.716667}},
{"country":"UA","name":"Laspi","_id":703363,"coord":{"lon":33.733334,"lat":44.416668}},
{"country":"VE","name":"Merida","_id":3632308,"coord":{"lon":-71.144997,"lat":8.598333}},
{"country":"RU","name":"Vinogradovo","_id":473537,"coord":{"lon":38.545555,"lat":55.423332}},
{"country":"IQ","name":"Qarah Gawl al ‘Ulyā","_id":384848,"coord":{"lon":45.6325,"lat":35.353889}},
{"country":"RU","name":"Cherkizovo","_id":569143,"coord":{"lon":37.728889,"lat":55.800835}},
{"country":"UA","name":"Alupka","_id":713514,"coord":{"lon":34.049999,"lat":44.416668}},
{"country":"DE","name":"Lichtenrade","_id":2878044,"coord":{"lon":13.40637,"lat":52.398441}},
{"country":"RU","name":"Zavety Il’icha","_id":464176,"coord":{"lon":37.849998,"lat":56.049999}},
{"country":"IL","name":"‘Azriqam","_id":295582,"coord":{"lon":34.700001,"lat":31.75}},
{"country":"IN","name":"Ghūra","_id":1271231,"coord":{"lon":79.883331,"lat":24.766666}}
]
These are only few entries, I have almost 1000 entries into file.
I need to apply pagination to load 100 entries.
I created codable as follows
struct Cities: Codable {
let city: [City]?
let pagination: Pagination?
}
struct City: Codable{
let country : String
let name : String
let _id : Int
let coord: [Coordinates]?
}
struct Coordinates: Codable {
let lat : Double?
let lon : Double?
}
struct Pagination: Codable {
let limit, offset: Int?
}
and my parsing method is like this,
func getDataFrom(completion: #escaping (Cities?, Error?) -> Void) {
let url = Bundle.main.url(forResource: "cities", withExtension: "json")!
let data = try! Data(contentsOf: url)
do {
let jsonDescription = try JSONDecoder().decode(Cities.self, from: data)
print(jsonDescription)
completion(jsonDescription,nil)
}
catch let jsonError {
print("Json Error:", jsonError)
}
}
When I parse the data it goes into catch block and gives this error
Json Error: typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Dictionary<String, Any> but found an array instead.", underlyingError: nil))
Can anyone tell me what I am doing wrong.
I need to parse this data and show on tableview with pagination.
Thanks in advance.
You are trying to decode a type of Cities.self, but your JSON is an array - it starts with "[" and ends with "]". You might want to try declaring the type [Cities].self in your decoder call.
let jsonDescription = try JSONDecoder().decode([City].self, from: data)
Edit:
#Joakim_Danielson caught an issue, you need to decode the array [City] and not [Cities], because that's what your JSON is (I edited the above line of code accordingly).
But there is another issue: the property coord in your struct City is declared as an array [Coordinates]?, but your JSON does not have an array in the "coord" key - just a single instance.
Try changing your coord property, make it a type of Coordinate?.

Swift: Parsing a JSON file where you don't know the key values

I'm building an app that queries wikidata using json. It works so far, but the issue I'm running into is the response I get from wikidata isn't the actual data, but an identifier. Then, to convert that identifier, I need to send it to another url and receive another json response, but the issue I'm running into is the the key value isn't something I know until I get the first json response in. So, lets say my key is Q1169621. When I run it through the api, I get this response:
I'm using codable and JSONDecoder, but I don't know how to tell the decoder the value of the key in entities is Q1169621 to get at the value I want ("Jim Lauderdale")...some of my code is below, I have structs to define the data of the response, but how do I replace the key value in my struct with the one defined from the previous json that is decoded?
struct InfoFromWikiConverted: Codable {
let entities: Locator //this is the value I need to set before parsing the json
}
struct Locator: Codable {
let labels: Labels //how do I link this to the struct above?
}
struct Labels: Codable {
let en: EN
}
struct EN: Codable {
let value: String
}
The simplest approach would be to decode entities as [String: Locator]:
struct InfoFromWikiConverted: Decodable {
let entities: [String: Locator]
}
Of course, if you want your model to just be a single Locator (which means, potentially ignoring multiple keys under entities), then you'd need to manually decode it.
You'd need to create a type to represent any string Coding Key and implement init(from:):
struct InfoFromWikiConverted: Decodable {
let entities: Locator
struct CodingKeys: CodingKey {
var stringValue: String
var intValue: Int? = nil
init(stringValue: String) { self.stringValue = stringValue }
init?(intValue: Int) { return nil }
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
// get the first key (ignore the rest), and throw if there are none
guard let key = container.allKeys.first else {
throw DecodingError.dataCorrupted(
.init(codingPath: container.codingPath,
debugDescription: "Expected a non-empty object"))
}
let entities = try container.decode(Locator.self, forKey: key)
}
}
Note that because you aren't retaining the ID, you can't encode it back to the same form, so I only conformed to Decodable instead of Codable

Swift: Check if data response is an array or a dictonary

I'm calling an API and storing the data in an array.
But if there is no data the debugger says:
Expected to decode Array but found a dictionary instead.
The fail JSON response is:
'{"status":"Failed","msg":"Sorry It\'s Not Working"}'
The success JSON response is:
'[{"id":"509","name":"TEC TEST !"#!12","sortingId":"1"},
{"id":"510","name":"TEC TEST !"#!12","sortingId":"2"},
{"id":"511","name":"TEC TEST !"#!12","sortingId":"3"},
{"id":"512","name":"TEC TEST !"#!12","sortingId":"4"},
{"id":"513","name":"TEC TEST !"#!12","sortingId":"5"},
{"id":"514","name":"TEC TEST !"#!12","sortingId":"6"},
{"id":"519","name":"TEC TEST !"#!12","sortingId":"7"}]'
So I want to switch between fetching my response as
var result:[Items]?
and
var result:Items?
if the Failed JSON gets send
I've been google´ing and searching Stackoverflow without luck
Is there an solution to say if JSON is an array or dictionary?
My Struct:
struct Items: Codable {
let id: String?
let sortingId: String?
let name: String?
let response: String?
let status: String?
let msg: String?
}
My processing of the response:
var result:[Items]?
result = try JSONDecoder().decode([Items].self, from: data!)
DispatchQueue.main.async {
for item in result! {
self.itemArray.append((name: item.name!, id: Int(item.id!)!, sortingId: Int(item.sortingId!)!))
}
}
One solution is to write a custom initializer which conditionally decodes the array or the dictionary.
Please ask the owner of the service to send more consistent JSON. It's very bad. At least the object should be always a dictionary with key status and either the array for key items or the key msg.
This code first tries to decode the array with unkeyedContainer. If it fails it decodes the dictionary.
struct Item: Decodable {
let id: String
let sortingId: String
let name: String
}
struct ItemData : Decodable {
private enum CodingKeys: String, CodingKey { case status, msg }
let status : String?
let msg: String?
var items = [Item]()
init(from decoder: Decoder) throws {
do {
var unkeyedContainer = try decoder.unkeyedContainer()
while !unkeyedContainer.isAtEnd {
items.append(try unkeyedContainer.decode(Item.self))
}
status = nil; msg = nil
} catch DecodingError.typeMismatch {
let container = try decoder.container(keyedBy: CodingKeys.self)
status = try container.decodeIfPresent(String.self, forKey: .status)
msg = try container.decodeIfPresent(String.self, forKey: .msg)
}
}
}
And call it
result = try JSONDecoder().decode(ItemData.self, from: data!)
A – probably more suitable – alternative is to catch the error in the JSONDecoder().decode line and use two simple structs
struct Item: Decodable {
let id: String
let sortingId: String
let name: String
}
struct ErrorData : Decodable {
let status : String
let msg: String
}
and call it
do {
let decoder = JSONDecoder()
do {
let result = try decoder.decode([Item].self, from: data!)
print(result)
} catch DecodingError.typeMismatch {
let result = try decoder.decode(ErrorData.self, from: data!)
print(result)
}
} catch { print(error) }
A big benefit is that all properties can be non-optional.
First of all the JSON does not contain any array. It's very very easy to read JSON. There are only 2 (two!) collection types, array [] and dictionary {}. As you can see there are no square brackets at all in the JSON string.
it will helpful

swift - convert json type Int to String

I have json data like this code below:
{
"all": [
{
"ModelId": 1,
"name": "ghe",
"width": 2
},
{
"ModelId": 2,
"name": "ban",
"width": 3
}]
}
I try to get the modelId and convert it to String but it's not working with my code:
let data = NSData(contentsOf: URL(string: url)!)
do {
if let data = data, let json = try JSONSerialization.jsonObject(with: data as Data) as? [String: Any], let models = json["all"] as? [[String:Any]] {
for model in models {
if let name = model["ModelId"] as? String {
_modelList.append(name)
}
}
}
completion(_modelList)
}catch {
print("error")
completion(nil)
}
How to fix this issue? Thanks.
I think ModelId is integer type. So, can you try to cast it to Integer
for model in models {
if let name = model["ModelId"] as? Int{
_modelList.append("\(name)")
}
}
Hope, it will help you.
if let as? is to unwrap, not type casting. So you unwrap first, then you cast it into string.
for model in models {
if let name = model["ModelId"] as? Int {
_modelList.append("\(name)")
}
}
Currently you are looking for a wrong key ,
for model in models {
if let name = model["ModelId"] as? NSNumber {
_modelList.append(name.stringValue)
}
}
As long as you are using JSONSerialization.jsonObject to parse your JSON you have very little control over the type the deserialiser will create, you basically let the parser decide. Sensible as it is it will create "some kind of " NSNumber of an Int type from a number without quotes. This can not be cast to a String, therefore your program will fail.
You can do different things in order to "fix" this problem, I would like to suggest the Codable protocol for JSON-parsing, but this specific problem can probably only be solved using a custom initialiser which looks kind of verbose as can be seen in this question.
If you just want to convert your NSNumber ModelId to a String you will have to create a new object (instead of trying to cast in vain). In your context this might simply be
if let name = String(model["ModelId"]) { ...
This is still not an elegant solution, however it will solve the problem at hand.
another approach is:
import Foundation
struct IntString: Codable
{
var value: String = "0"
init(from decoder: Decoder) throws
{
// get this instance json value
let container = try decoder.singleValueContainer()
do
{
// try to parse the value as int
value = try String(container.decode(Int.self))
}
catch
{
// if we failed parsing the value as int, try to parse it as a string
value = try container.decode(String.self)
}
}
func encode(to encoder: Encoder) throws
{
var container = encoder.singleValueContainer()
try container.encode(value)
}
}
my solution is to create a new struct that will be able to receive either a String or and Int and parse it as a string, this way in my code i can decide how to treat it, and when my server sends me sometimes an Int value and sometimes a json with that same key as a String value - the parser can parse it without failing
of course you can do it with any type (date / double / float / or even a full struct), and even insert it with some logic of your own (say get the string value of an enum based on the received value and use it as index or whatever)
so your code should look like this:
import Foundation
struct Models: Codable {
let all: [All]
}
struct All: Codable {
let modelID: IntString
let name: String
let width: IntString
enum CodingKeys: String, CodingKey {
case modelID = "ModelId"
case name = "name"
case width = "width"
}
}
parse json into the Models struct:
let receivedModel: Decodable = Bundle.main.decode(Models.self, from: jsonData!)
assuming you'r json decoder is:
import Foundation
extension Bundle
{
func decode<T: Decodable>(_ type: T.Type, from jsonData: Data, dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .deferredToDate, keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys) -> T
{
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = dateDecodingStrategy
decoder.keyDecodingStrategy = keyDecodingStrategy
do
{
return try decoder.decode(T.self, from: jsonData)
}
catch DecodingError.keyNotFound(let key, let context)
{
fatalError("Failed to decode \(jsonData) from bundle due to missing key '\(key.stringValue)' not found – \(context.debugDescription)")
}
catch DecodingError.typeMismatch(let type, let context)
{
print("Failed to parse type: \(type) due to type mismatch – \(context.debugDescription) the received JSON: \(String(decoding: jsonData, as: UTF8.self))")
fatalError("Failed to decode \(jsonData) from bundle due to type mismatch – \(context.debugDescription)")
}
catch DecodingError.valueNotFound(let type, let context)
{
fatalError("Failed to decode \(jsonData) from bundle due to missing \(type) value – \(context.debugDescription)")
}
catch DecodingError.dataCorrupted(_)
{
fatalError("Failed to decode \(jsonData) from bundle because it appears to be invalid JSON")
}
catch
{
fatalError("Failed to decode \(jsonData) from bundle: \(error.localizedDescription)")
}
}
}