I follow a lesson from one course
And I need to get json, but i want get another json than in a lesson.
So this is my json:
https://api.scryfall.com/cards/search?q=half
And code:
struct Card {
var cardId: String
var name: String
var imageUrl: String
var text: String
init?(dict: [String: AnyObject]){
guard let name = dict["name"] as? String,
let cardId = dict["cardId"] as? String,
let imageUrl = dict["imageUrl"] as? String,
let text = dict["text"] as? String else { return nil }
self.cardId = cardId
self.name = name
self.imageUrl = imageUrl
self.text = text
}
}
class CardNetworkService{
private init() {}
static func getCards(url: String, completion: #escaping(GetCardResponse) -> ()) {
guard let url = URL(string: url) else { return }
NetworkService.shared.getData(url: url) { (json) in
do {
print ("ok1")
let response = try GetCardResponse(json: json)
print ("ok2")
completion(response)
} catch {
print(error)
}
}
}
}
class NetworkService {
private init() {}
static let shared = NetworkService()
func getData(url: URL, completion: #escaping (Any) -> ()) {
let session = URLSession.shared
session.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
do {
let json = try JSONSerialization.jsonObject(with: data, options: [])
DispatchQueue.main.async {
completion(json)
}
print(json)
} catch {
print(error)
}
}.resume()
}
}
struct GetCardResponse{
let cards: [Card]
init(json: Any) throws {
guard let array = json as? [[String: AnyObject]] else { throw NetworkError.failInternetError }
var cards = [Card]()
for dictionary in array {
guard let card = Card(dict: dictionary) else { continue }
cards.append(card)
}
self.cards = cards
}
}
Problem in struct GetCardResponse and [[String: AnyObject]] because I dont know how to parse this type of json. I tried to change them in the likeness of json. But I dont really understand how it works and in which part of code i need to put json["data"] or something like this... Help pls. I just want get json fields tcgplayer_id, name, art_crop
As of your code, you can parse the required details as:
struct Card {
var cardId: String = ""
var name: String = ""
var imageUrl: String = ""
var text: String = ""
init(dict: [String: Any]) {
if let obj = dict["name"] {
self.name = "\(obj)"
}
if let obj = dict["tcgplayer_id"] {
self.cardId = "\(obj)"
}
if let obj = dict["image_uris"] as? [String:Any], let url = obj["art_crop"] {
self.imageUrl = "\(url)"
}
if let obj = dict["oracle_text"] {
self.text = "\(obj)"
}
}
static func models(array: [[String:Any]]) -> [Card] {
return array.map { Card(dict: $0) }
}
}
class CardNetworkService{
private init() {}
static func getCards(url: String, completion: #escaping([Card]?) -> ()) {
guard let url = URL(string: url) else { return }
NetworkService.shared.getData(url: url) { (json) in
print ("ok1")
if let jData = json as? [String:Any], let data = jData["data"] as? [[String:Any]] {
let response = Card.models(array: data)
completion(response)
}
completion(nil)
}
}
}
class NetworkService {
private init() {}
static let shared = NetworkService()
func getData(url: URL, completion: #escaping (Any) -> ()) {
let session = URLSession.shared
session.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
do {
let json = try JSONSerialization.jsonObject(with: data, options: [])
DispatchQueue.main.async {
completion(json)
}
} catch {
print(error)
}
}.resume()
}
}
CardNetworkService.getCards(url: "https://api.scryfall.com/cards/search?q=half") { (res) in
print(res ?? [])
}
Just paste this code in playground and it'll work.
Happy Coding :)
You are wrong get entry of data field.
First you need get data field in json. And parse to deeper.
Try use the code.
struct GetCardResponse{
let cards: [Card]
init(json: Any) throws {
guard let jsonObject = json as? [String: Any], let data = jsonObject["data"] as? [[String:AnyObject]] else { throw NetworkError.failInternetError }
var cards = [Card]()
for dictionary in data {
guard let card = Card(dict: dictionary) else { continue }
cards.append(card)
}
self.cards = cards
}
}
UPDATE:
init function in Card has something wrong. In your json cardId is not found
Card class maybe like this because cardId, imageUrl, text maybe not found. It is optional
struct Card {
var cardId: String?
var name: String
var imageUrl: String?
var text: String?
init?(dict: [String: AnyObject]){
guard let name = dict["name"] as? String else { return nil }
self.cardId = dict["cardId"] as? String
self.name = name
self.imageUrl = dict["imageUrl"] as? String
self.text = dict["text"] as? String
}
}
Try using Codable to parse the JSON data like so,
Create the models like,
struct Root: Decodable {
let cards: [Card]
enum CodingKeys: String, CodingKey {
case cards = "data"
}
}
struct Card: Decodable {
let tcgplayerId: Int
let name: String
let artCrop: String
}
Now parse your JSON data using,
if let data = data {
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let response = try JSONDecoder().decode(Root.self, from: data)
print(response)
} catch {
print(error)
}
}
You can access the properties in cards of response like so,
response.cards.first?.tcgplayerId
I am wondering what i am doing wrong . I am trying to understand how to use urlsession and codable protocol using JSONDecoder. When i use JSONDecoder i am getting the following error message :
keyNotFound(CodingKeys(stringValue: "name", intValue: nil), my resaponse contain ''name'' . But when i use JSONSerialization, I am able to print the response . If someone can explain me.
Code using JSONDecoder
struct Business:Codable {
let name: String
enum CodingKeys: String, CodingKey {
case name = "name"
}
init(from decoder: Decoder) throws {
let value = try decoder.container(keyedBy: CodingKeys.self)
self.name = try value.decode(String.self, forKey:CodingKeys.name)
}
}
let task = session.dataTask(with: request) { (data, response, error) in
if let response = response as? HTTPURLResponse {
print(response)
} else{
print("error")
}
guard let data = data else {return}
do {
let business = try JSONDecoder().decode(Business.self, from: data)
print(business.name)
} catch {
print("Error parsing JSON: \(error)")
}
}
task.resume()
Code using JSONSerialization
struct Business: Decodable {
let name: String
let displayAddress: String
let categories: String
let imageUrl : String
init(json: [String:Any]) {
name = json["name"] as? String ?? ""
displayAddress = json["location"] as? String ?? ""
categories = json["categories"] as? String ?? ""
imageUrl = json["image_url"] as? String ?? ""
}
}
let task = session.dataTask(with: request) { (data, response, error) in
if let response = response as? HTTPURLResponse {
print(response)
} else{
print("error")
}
guard let data = data else {return}
do {
if let myjson = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? Dictionary<String,Any> {
print(myjson)
}
} catch {
print("Error parsing ")
}
}
task.resume()
The response
["region": {
center = {
latitude = "43.67428196976998";
longitude = "-79.39682006835938";
};
}, "businesses": <__NSArrayM 0x60000211cff0>(
{
alias = "pai-northern-thai-kitchen-toronto-5";
categories = (
{
alias = thai;
title = Thai;
}
);
coordinates = {
latitude = "43.647866";
longitude = "-79.38864150000001";
};
"display_phone" = "+1 416-901-4724";
distance = "3010.095870925626";
id = "r_BrIgzYcwo1NAuG9dLbpg";
"image_url" = "https://s3-media3.fl.yelpcdn.com/bphoto/t-g4d_vCAgZH_6pCqjaYWQ/o.jpg";
"is_closed" = 0;
location = {
address1 = "18 Duncan Street";
address2 = "";
address3 = "";
city = Toronto;
country = CA;
"display_address" = (
"18 Duncan Street",
"Toronto, ON M5H 3G8",
Canada
);
state = ON;
"zip_code" = "M5H 3G8";
};
name = "Pai Northern Thai Kitchen";
phone = "+14169014724";
price = "$$";
rating = "4.5";
"review_count" = 2405;
transactions = (
);
url = "https://www.yelp.com/biz/pai-northern-thai-kitchen-toronto-5?adjust_creative=A4ydpSOHv8wBNquTDeh0DQ&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_search&utm_source=A4ydpSOHv8wBNquTDeh0DQ";
},
Business is not the root data object in your JSON. You need something like this:
struct Business: Codable {
let name: String
}
struct RootObject: Codable {
let businesses: [Business]
}
let rootObject = try JSONDecoder().decode(RootObject.self, from: data)
print(rootObject.businesses.first?.name)
I am fairly new to SWIFT and am trying to use data from a JSON file (financetoday.json) to populate a table and for the user to update and have the data stored on the device. The table has collapsing cells (using XIB) and each cell has an embedded UISlider to update the values. On my initial work, I have the table loading the JSON file successfully from the bundle, populating the table, and the slider changes each value. Now the hard part. In order to save/change the data I need to move the JSON file into the documentDirectory, then have any changes to the data from this file. Once the user starts the app for the first time, I no longer need to use the JSON file in the bundle, just the version in the documentDirectory. I have been unable to get table to read the JSON file in the documentDirectory. Any help would be appreciated.
Here is where I have added a method in AppDelegate to move the JSON file in the document Directory
// Move json file from bundle to documents folder
var finalDatabaseURL:String = String()
func copyDatabaseIfNeeded() {
let fileManager = FileManager.default
let documentsUrl = fileManager.urls(for: .documentDirectory,
in: .userDomainMask)
guard documentsUrl.count != 0 else {
return // Could not find documents URL
}
let finalDatabaseURL = documentsUrl.first!.appendingPathComponent("financetoday.json")
if !( (try? finalDatabaseURL.checkResourceIsReachable()) ?? false) {
print("DB does not exist in documents folder")
let documentsURL = Bundle.main.resourceURL?.appendingPathComponent("financetoday.json")
do {
try fileManager.copyItem(atPath: (documentsURL?.path)!, toPath: finalDatabaseURL.path)
} catch let error as NSError {
print("Couldn't copy file to final location! Error:\(error.description)")
}
} else {
print("Database file found at path: \(finalDatabaseURL.path)")
}
}
Then I added to applicationDidBecomeActive
self.copyDatabaseIfNeeded()
In my data model this is what it looks like loading JSON data from the bundle, but I need to change the code in method dataFromFile to use JSON file in documentDirectory...not the bundle. All my attempts to change results in a blank table. So for now I am pointing to the JSON in the bundle. Any help would be appreciated.
import Foundation
public func dataFromFile(_ filename: String) -> Data? {
#objc class TestClass: NSObject { }
let bundle = Bundle(for: TestClass.self)
if let path = bundle.path(forResource: filename, ofType: "json") {
return (try? Data(contentsOf: URL(fileURLWithPath: path)))
}
return nil
}
class Plan {
var yeardata: Int?
var incomedata = [Income]()
var expensedata = [Expense]()
var assetdata = [Asset]()
var liabilitydata = [Liability]()
var profiledata = [Profile]()
var assumptiondata = [Assumption]()
init?(data: Data) {
do {
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], let myplan = json["data"] as? [String: Any] {
if let incomedata = myplan["incomedata"] as? [[String: Any]] {
self.incomedata = incomedata.map { Income(json: $0) }
}
if let expensedata = myplan["expensedata"] as? [[String: Any]] {
self.expensedata = expensedata.map { Expense(json: $0) }
}
if let assetdata = myplan["assetdata"] as? [[String: Any]] {
self.assetdata = assetdata.map { Asset(json: $0) }
}
if let liabilitydata = myplan["liabilitydata"] as? [[String: Any]] {
self.liabilitydata = liabilitydata.map { Liability(json: $0) }
}
if let profiledata = myplan["profiledata"] as? [[String: Any]] {
self.profiledata = profiledata.map { Profile(json: $0) }
}
if let assumptiondata = myplan["assumptiondata"] as? [[String: Any]] {
self.assumptiondata = assumptiondata.map { Assumption(json: $0) }
}
}
} catch {
print("Error deserializing JSON: \(error)")
return nil
}
}
}
class Income {
var key: String?
var value: Any?
init(json: [String: Any]) {
self.key = json["key"] as? String
self.value = json["value"] as Any
}
}
class Expense {
var key: String?
var value: Any?
init(json: [String: Any]) {
self.key = json["key"] as? String
self.value = json["value"] as Any
}
}
class Asset {
var key: String?
var value: Any?
init(json: [String: Any]) {
self.key = json["key"] as? String
self.value = json["value"] as Any
}
}
class Liability {
var key: String?
var value: Any?
init(json: [String: Any]) {
self.key = json["key"] as? String
self.value = json["value"] as Any
}
}
class Profile {
var key: String?
var value: Any?
init(json: [String: Any]) {
self.key = json["key"] as? String
self.value = json["value"] as Any
}
}
class Assumption {
var key: String?
var value: Any?
init(json: [String: Any]) {
self.key = json["key"] as? String
self.value = json["value"] as Any
}
}
This will read in the json. The dictionary conversion I am less familiar with because I have started using the Codable protocol which I highly recommend.
if let path = Bundle.main.path(forResource: "FileName", ofType: "json") {
do {
let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .alwaysMapped)
let jsonString = String(data: value, encoding: .utf8)
print("json as string: \(jsonString)")
let json = try JSONSerialization.data(withJSONObject: data, options: []) as? [String: Any]
print("json as dictionary: \(json)")
} catch let error {
print("parse error: \(error.localizedDescription)")
}
}
How to decode codable Data:
let decoder = JSONDecoder()
do {
let decodableJSON = try decoder.decode(ObjectConformingToCodable.self, from: data)
print(decodableJSON)
} catch let error {
print(error.localizedDescription)
}
not sure if this is relevant but could try:
let documentsDirectoryPathString = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!
let documentsDirectoryPath = NSURL(string: documentsDirectoryPathString)!
let jsonFilePath = documentsDirectoryPath.appendingPathComponent("test.json")
let fileManager = FileManager.default
var isDirectory: ObjCBool = false
// creating a .json file in the Documents folder
if fileManager.fileExists(atPath: (jsonFilePath?.absoluteString)!, isDirectory: &isDirectory) {
print("File exists")
}
I also found: JSONSaveLoad.swift on Gist:
https://gist.github.com/norsez/aa3f11c0e875526e5270e7791f3891fb
I'm sure there are other similar examples on Github
Let's say I have Customer data type which contains a metadata property that can contains any JSON dictionary in the customer object
struct Customer {
let id: String
let email: String
let metadata: [String: Any]
}
{
"object": "customer",
"id": "4yq6txdpfadhbaqnwp3",
"email": "john.doe#example.com",
"metadata": {
"link_id": "linked-id",
"buy_count": 4
}
}
The metadata property can be any arbitrary JSON map object.
Before I can cast the property from a deserialized JSON from NSJSONDeserialization but with the new Swift 4 Decodable protocol, I still can't think of a way to do that.
Do anyone know how to achieve this in Swift 4 with Decodable protocol?
With some inspiration from this gist I found, I wrote some extensions for UnkeyedDecodingContainer and KeyedDecodingContainer. You can find a link to my gist here. By using this code you can now decode any Array<Any> or Dictionary<String, Any> with the familiar syntax:
let dictionary: [String: Any] = try container.decode([String: Any].self, forKey: key)
or
let array: [Any] = try container.decode([Any].self, forKey: key)
Edit: there is one caveat I have found which is decoding an array of dictionaries [[String: Any]] The required syntax is as follows. You'll likely want to throw an error instead of force casting:
let items: [[String: Any]] = try container.decode(Array<Any>.self, forKey: .items) as! [[String: Any]]
EDIT 2: If you simply want to convert an entire file to a dictionary, you are better off sticking with api from JSONSerialization as I have not figured out a way to extend JSONDecoder itself to directly decode a dictionary.
guard let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
// appropriate error handling
return
}
The extensions
// Inspired by https://gist.github.com/mbuchetics/c9bc6c22033014aa0c550d3b4324411a
struct JSONCodingKeys: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int?
init?(intValue: Int) {
self.init(stringValue: "\(intValue)")
self.intValue = intValue
}
}
extension KeyedDecodingContainer {
func decode(_ type: Dictionary<String, Any>.Type, forKey key: K) throws -> Dictionary<String, Any> {
let container = try self.nestedContainer(keyedBy: JSONCodingKeys.self, forKey: key)
return try container.decode(type)
}
func decodeIfPresent(_ type: Dictionary<String, Any>.Type, forKey key: K) throws -> Dictionary<String, Any>? {
guard contains(key) else {
return nil
}
guard try decodeNil(forKey: key) == false else {
return nil
}
return try decode(type, forKey: key)
}
func decode(_ type: Array<Any>.Type, forKey key: K) throws -> Array<Any> {
var container = try self.nestedUnkeyedContainer(forKey: key)
return try container.decode(type)
}
func decodeIfPresent(_ type: Array<Any>.Type, forKey key: K) throws -> Array<Any>? {
guard contains(key) else {
return nil
}
guard try decodeNil(forKey: key) == false else {
return nil
}
return try decode(type, forKey: key)
}
func decode(_ type: Dictionary<String, Any>.Type) throws -> Dictionary<String, Any> {
var dictionary = Dictionary<String, Any>()
for key in allKeys {
if let boolValue = try? decode(Bool.self, forKey: key) {
dictionary[key.stringValue] = boolValue
} else if let stringValue = try? decode(String.self, forKey: key) {
dictionary[key.stringValue] = stringValue
} else if let intValue = try? decode(Int.self, forKey: key) {
dictionary[key.stringValue] = intValue
} else if let doubleValue = try? decode(Double.self, forKey: key) {
dictionary[key.stringValue] = doubleValue
} else if let nestedDictionary = try? decode(Dictionary<String, Any>.self, forKey: key) {
dictionary[key.stringValue] = nestedDictionary
} else if let nestedArray = try? decode(Array<Any>.self, forKey: key) {
dictionary[key.stringValue] = nestedArray
}
}
return dictionary
}
}
extension UnkeyedDecodingContainer {
mutating func decode(_ type: Array<Any>.Type) throws -> Array<Any> {
var array: [Any] = []
while isAtEnd == false {
// See if the current value in the JSON array is `null` first and prevent infite recursion with nested arrays.
if try decodeNil() {
continue
} else if let value = try? decode(Bool.self) {
array.append(value)
} else if let value = try? decode(Double.self) {
array.append(value)
} else if let value = try? decode(String.self) {
array.append(value)
} else if let nestedDictionary = try? decode(Dictionary<String, Any>.self) {
array.append(nestedDictionary)
} else if let nestedArray = try? decode(Array<Any>.self) {
array.append(nestedArray)
}
}
return array
}
mutating func decode(_ type: Dictionary<String, Any>.Type) throws -> Dictionary<String, Any> {
let nestedContainer = try self.nestedContainer(keyedBy: JSONCodingKeys.self)
return try nestedContainer.decode(type)
}
}
I have played with this problem, too, and finally wrote a simple library for working with “generic JSON” types. (Where “generic” means “with no structure known in advance”.) Main point is representing the generic JSON with a concrete type:
public enum JSON {
case string(String)
case number(Float)
case object([String:JSON])
case array([JSON])
case bool(Bool)
case null
}
This type can then implement Codable and Equatable.
You can create metadata struct which conforms to Decodable protocol and use JSONDecoder class to create object from data by using decode method like below
let json: [String: Any] = [
"object": "customer",
"id": "4yq6txdpfadhbaqnwp3",
"email": "john.doe#example.com",
"metadata": [
"link_id": "linked-id",
"buy_count": 4
]
]
struct Customer: Decodable {
let object: String
let id: String
let email: String
let metadata: Metadata
}
struct Metadata: Decodable {
let link_id: String
let buy_count: Int
}
let data = try JSONSerialization.data(withJSONObject: json, options: .prettyPrinted)
let decoder = JSONDecoder()
do {
let customer = try decoder.decode(Customer.self, from: data)
print(customer)
} catch {
print(error.localizedDescription)
}
I came with a slightly different solution.
Let's suppose we have something more than a simple [String: Any] to parse were Any might be an array or a nested dictionary or a dictionary of arrays.
Something like this:
var json = """
{
"id": 12345,
"name": "Giuseppe",
"last_name": "Lanza",
"age": 31,
"happy": true,
"rate": 1.5,
"classes": ["maths", "phisics"],
"dogs": [
{
"name": "Gala",
"age": 1
}, {
"name": "Aria",
"age": 3
}
]
}
"""
Well, this is my solution:
public struct AnyDecodable: Decodable {
public var value: Any
private struct CodingKeys: CodingKey {
var stringValue: String
var intValue: Int?
init?(intValue: Int) {
self.stringValue = "\(intValue)"
self.intValue = intValue
}
init?(stringValue: String) { self.stringValue = stringValue }
}
public init(from decoder: Decoder) throws {
if let container = try? decoder.container(keyedBy: CodingKeys.self) {
var result = [String: Any]()
try container.allKeys.forEach { (key) throws in
result[key.stringValue] = try container.decode(AnyDecodable.self, forKey: key).value
}
value = result
} else if var container = try? decoder.unkeyedContainer() {
var result = [Any]()
while !container.isAtEnd {
result.append(try container.decode(AnyDecodable.self).value)
}
value = result
} else if let container = try? decoder.singleValueContainer() {
if let intVal = try? container.decode(Int.self) {
value = intVal
} else if let doubleVal = try? container.decode(Double.self) {
value = doubleVal
} else if let boolVal = try? container.decode(Bool.self) {
value = boolVal
} else if let stringVal = try? container.decode(String.self) {
value = stringVal
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "the container contains nothing serialisable")
}
} else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Could not serialise"))
}
}
}
Try it using
let stud = try! JSONDecoder().decode(AnyDecodable.self, from: jsonData).value as! [String: Any]
print(stud)
When I found the old answer, I only tested a simple JSON object case but not an empty one which will cause a runtime exception like #slurmomatic and #zoul found. Sorry for this issue.
So I try another way by having a simple JSONValue protocol, implement the AnyJSONValue type erasure struct and use that type instead of Any. Here's an implementation.
public protocol JSONType: Decodable {
var jsonValue: Any { get }
}
extension Int: JSONType {
public var jsonValue: Any { return self }
}
extension String: JSONType {
public var jsonValue: Any { return self }
}
extension Double: JSONType {
public var jsonValue: Any { return self }
}
extension Bool: JSONType {
public var jsonValue: Any { return self }
}
public struct AnyJSONType: JSONType {
public let jsonValue: Any
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let intValue = try? container.decode(Int.self) {
jsonValue = intValue
} else if let stringValue = try? container.decode(String.self) {
jsonValue = stringValue
} else if let boolValue = try? container.decode(Bool.self) {
jsonValue = boolValue
} else if let doubleValue = try? container.decode(Double.self) {
jsonValue = doubleValue
} else if let doubleValue = try? container.decode(Array<AnyJSONType>.self) {
jsonValue = doubleValue
} else if let doubleValue = try? container.decode(Dictionary<String, AnyJSONType>.self) {
jsonValue = doubleValue
} else {
throw DecodingError.typeMismatch(JSONType.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Unsupported JSON tyep"))
}
}
}
And here is how to use it when decoding
metadata = try container.decode ([String: AnyJSONValue].self, forKey: .metadata)
The problem with this issue is that we must call value.jsonValue as? Int. We need to wait until Conditional Conformance land in Swift, that would solve this problem or at least help it to be better.
[Old Answer]
I post this question on the Apple Developer forum and it turns out it is very easy.
I can do
metadata = try container.decode ([String: Any].self, forKey: .metadata)
in the initializer.
It was my bad to miss that in the first place.
If you use SwiftyJSON to parse JSON, you can update to 4.1.0 which has Codable protocol support. Just declare metadata: JSON and you're all set.
import SwiftyJSON
struct Customer {
let id: String
let email: String
let metadata: JSON
}
I have written an article and repo that helps in adding [String: Any] support for Codable for decoding as well as encoding.
https://medium.com/nerd-for-tech/string-any-support-for-codable-4ba062ce62f2
This improves on decodable aspect and also add encodable support as solution given by in https://stackoverflow.com/a/46049763/9160905
what you will be able to achieve:
json:
sample code:
You might have a look at BeyovaJSON
import BeyovaJSON
struct Customer: Codable {
let id: String
let email: String
let metadata: JToken
}
//create a customer instance
customer.metadata = ["link_id": "linked-id","buy_count": 4]
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
print(String(bytes: try! encoder.encode(customer), encoding: .utf8)!)
Here is more generic (not only [String: Any], but [Any] can decoded) and encapsulated approach (separate entity is used for that) inspired by #loudmouth answer.
Using it will look like:
extension Customer: Decodable {
public init(from decoder: Decoder) throws {
let selfContainer = try decoder.container(keyedBy: CodingKeys.self)
id = try selfContainer.decode(.id)
email = try selfContainer.decode(.email)
let metadataContainer: JsonContainer = try selfContainer.decode(.metadata)
guard let metadata = metadataContainer.value as? [String: Any] else {
let context = DecodingError.Context(codingPath: [CodingKeys.metadata], debugDescription: "Expected '[String: Any]' for 'metadata' key")
throw DecodingError.typeMismatch([String: Any].self, context)
}
self.metadata = metadata
}
private enum CodingKeys: String, CodingKey {
case id, email, metadata
}
}
JsonContainer is a helper entity we use to wrap decoding JSON data to JSON object (either array or dictionary) without extending *DecodingContainer (so it won't interfere with rare cases when a JSON object is not meant by [String: Any]).
struct JsonContainer {
let value: Any
}
extension JsonContainer: Decodable {
public init(from decoder: Decoder) throws {
if let keyedContainer = try? decoder.container(keyedBy: Key.self) {
var dictionary = [String: Any]()
for key in keyedContainer.allKeys {
if let value = try? keyedContainer.decode(Bool.self, forKey: key) {
// Wrapping numeric and boolean types in `NSNumber` is important, so `as? Int64` or `as? Float` casts will work
dictionary[key.stringValue] = NSNumber(value: value)
} else if let value = try? keyedContainer.decode(Int64.self, forKey: key) {
dictionary[key.stringValue] = NSNumber(value: value)
} else if let value = try? keyedContainer.decode(Double.self, forKey: key) {
dictionary[key.stringValue] = NSNumber(value: value)
} else if let value = try? keyedContainer.decode(String.self, forKey: key) {
dictionary[key.stringValue] = value
} else if (try? keyedContainer.decodeNil(forKey: key)) ?? false {
// NOP
} else if let value = try? keyedContainer.decode(JsonContainer.self, forKey: key) {
dictionary[key.stringValue] = value.value
} else {
throw DecodingError.dataCorruptedError(forKey: key, in: keyedContainer, debugDescription: "Unexpected value for \(key.stringValue) key")
}
}
value = dictionary
} else if var unkeyedContainer = try? decoder.unkeyedContainer() {
var array = [Any]()
while !unkeyedContainer.isAtEnd {
let container = try unkeyedContainer.decode(JsonContainer.self)
array.append(container.value)
}
value = array
} else if let singleValueContainer = try? decoder.singleValueContainer() {
if let value = try? singleValueContainer.decode(Bool.self) {
self.value = NSNumber(value: value)
} else if let value = try? singleValueContainer.decode(Int64.self) {
self.value = NSNumber(value: value)
} else if let value = try? singleValueContainer.decode(Double.self) {
self.value = NSNumber(value: value)
} else if let value = try? singleValueContainer.decode(String.self) {
self.value = value
} else if singleValueContainer.decodeNil() {
value = NSNull()
} else {
throw DecodingError.dataCorruptedError(in: singleValueContainer, debugDescription: "Unexpected value")
}
} else {
let context = DecodingError.Context(codingPath: [], debugDescription: "Invalid data format for JSON")
throw DecodingError.dataCorrupted(context)
}
}
private struct Key: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int?
init?(intValue: Int) {
self.init(stringValue: "\(intValue)")
self.intValue = intValue
}
}
}
Note that numberic and boolean types are backed by NSNumber, else something like this won't work:
if customer.metadata["keyForInt"] as? Int64 { // as it always will be nil
I have made a pod to facilitate the way the decoding + encoding [String: Any], [Any]. And this provides encode or decode the optional properties, here https://github.com/levantAJ/AnyCodable
pod 'DynamicCodable', '1.0'
How to use it:
import DynamicCodable
struct YourObject: Codable {
var dict: [String: Any]
var array: [Any]
var optionalDict: [String: Any]?
var optionalArray: [Any]?
enum CodingKeys: String, CodingKey {
case dict
case array
case optionalDict
case optionalArray
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
dict = try values.decode([String: Any].self, forKey: .dict)
array = try values.decode([Any].self, forKey: .array)
optionalDict = try values.decodeIfPresent([String: Any].self, forKey: .optionalDict)
optionalArray = try values.decodeIfPresent([Any].self, forKey: .optionalArray)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(dict, forKey: .dict)
try container.encode(array, forKey: .array)
try container.encodeIfPresent(optionalDict, forKey: .optionalDict)
try container.encodeIfPresent(optionalArray, forKey: .optionalArray)
}
}
Details
Xcode 12.0.1 (12A7300)
Swift 5.3
Based on Tai Le library
// code from: https://github.com/levantAJ/AnyCodable/blob/master/AnyCodable/DecodingContainer%2BAnyCollection.swift
private
struct AnyCodingKey: CodingKey {
let stringValue: String
private (set) var intValue: Int?
init?(stringValue: String) { self.stringValue = stringValue }
init?(intValue: Int) {
self.intValue = intValue
stringValue = String(intValue)
}
}
extension KeyedDecodingContainer {
private
func decode(_ type: [Any].Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> [Any] {
var values = try nestedUnkeyedContainer(forKey: key)
return try values.decode(type)
}
private
func decode(_ type: [String: Any].Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> [String: Any] {
try nestedContainer(keyedBy: AnyCodingKey.self, forKey: key).decode(type)
}
func decode(_ type: [String: Any].Type) throws -> [String: Any] {
var dictionary: [String: Any] = [:]
for key in allKeys {
if try decodeNil(forKey: key) {
dictionary[key.stringValue] = NSNull()
} else if let bool = try? decode(Bool.self, forKey: key) {
dictionary[key.stringValue] = bool
} else if let string = try? decode(String.self, forKey: key) {
dictionary[key.stringValue] = string
} else if let int = try? decode(Int.self, forKey: key) {
dictionary[key.stringValue] = int
} else if let double = try? decode(Double.self, forKey: key) {
dictionary[key.stringValue] = double
} else if let dict = try? decode([String: Any].self, forKey: key) {
dictionary[key.stringValue] = dict
} else if let array = try? decode([Any].self, forKey: key) {
dictionary[key.stringValue] = array
}
}
return dictionary
}
}
extension UnkeyedDecodingContainer {
mutating func decode(_ type: [Any].Type) throws -> [Any] {
var elements: [Any] = []
while !isAtEnd {
if try decodeNil() {
elements.append(NSNull())
} else if let int = try? decode(Int.self) {
elements.append(int)
} else if let bool = try? decode(Bool.self) {
elements.append(bool)
} else if let double = try? decode(Double.self) {
elements.append(double)
} else if let string = try? decode(String.self) {
elements.append(string)
} else if let values = try? nestedContainer(keyedBy: AnyCodingKey.self),
let element = try? values.decode([String: Any].self) {
elements.append(element)
} else if var values = try? nestedUnkeyedContainer(),
let element = try? values.decode([Any].self) {
elements.append(element)
}
}
return elements
}
}
Solution
struct DecodableDictionary: Decodable {
typealias Value = [String: Any]
let dictionary: Value?
init(from decoder: Decoder) throws {
dictionary = try? decoder.container(keyedBy: AnyCodingKey.self).decode(Value.self)
}
}
Usage
struct Model: Decodable {
let num: Double?
let flag: Bool?
let dict: DecodableDictionary?
let dict2: DecodableDictionary?
let dict3: DecodableDictionary?
}
let data = try! JSONSerialization.data(withJSONObject: dictionary)
let object = try JSONDecoder().decode(Model.self, from: data)
print(object.dict?.dictionary) // prints [String: Any]
print(object.dict2?.dictionary) // prints nil
print(object.dict3?.dictionary) // prints nil
I used some of the answers on this topic to get the simplest solution possible for me. My problem is that I was receiving a [String: Any] type dictionary, but I could very well work with a [String: String] transforming every other Any value in String. So this is my solution:
struct MetadataType: Codable {
let value: String?
private init(_ value: String?) {
self.value = value
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let decodedValue = try? container.decode(Int.self) {
self.init(String(decodedValue))
} else if let decodedValue = try? container.decode(Double.self) {
self.init(String(decodedValue))
} else if let decodedValue = try? container.decode(Bool.self) {
self.init(String(decodedValue))
} else if let decodedValue = try? container.decode(String.self) {
self.init(decodedValue)
} else {
self.init(nil)
}
}
}
And when declaring my dictionary, I use
let userInfo: [String: MetadataType]
The easiest and suggested way is to create separate model for each dictionary or model that is in JSON.
Here is what I do
//Model for dictionary **Metadata**
struct Metadata: Codable {
var link_id: String?
var buy_count: Int?
}
//Model for dictionary **Customer**
struct Customer: Codable {
var object: String?
var id: String?
var email: String?
var metadata: Metadata?
}
//Here is our decodable parser that decodes JSON into expected model
struct CustomerParser {
var customer: Customer?
}
extension CustomerParser: Decodable {
//keys that matches exactly with JSON
enum CustomerKeys: String, CodingKey {
case object = "object"
case id = "id"
case email = "email"
case metadata = "metadata"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CustomerKeys.self) // defining our (keyed) container
let object: String = try container.decode(String.self, forKey: .object) // extracting the data
let id: String = try container.decode(String.self, forKey: .id) // extracting the data
let email: String = try container.decode(String.self, forKey: .email) // extracting the data
//Here I have used metadata model instead of dictionary [String: Any]
let metadata: Metadata = try container.decode(Metadata.self, forKey: .metadata) // extracting the data
self.init(customer: Customer(object: object, id: id, email: email, metadata: metadata))
}
}
Usage:
if let url = Bundle.main.url(forResource: "customer-json-file", withExtension: "json") {
do {
let jsonData: Data = try Data(contentsOf: url)
let parser: CustomerParser = try JSONDecoder().decode(CustomerParser.self, from: jsonData)
print(parser.customer ?? "null")
} catch {
}
}
**I have used optional to be in safe side while parsing, can be changed as needed.
Read more on this topic
decode using decoder and coding keys
public let dataToDecode: [String: AnyDecodable]
enum CodingKeys: CodingKey {
case dataToDecode
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.dataToDecode = try container.decode(Dictionary<String, AnyDecodable>.self, forKey: .dataToDecode)
}
This will work
public struct AnyDecodable: Decodable {
public let value: Any
public init<T>(_ value: T?) {
self.value = value ?? ()
}
}
let contentDecodable = try values.decodeIfPresent(AnyDecodable.self, forKey: .content)
extension ViewController {
func swiftyJson(){
let url = URL(string: "https://itunes.apple.com/search?term=jack+johnson")
//let url = URL(string: "http://makani.bitstaging.in/api/business/businesses_list")
Alamofire.request(url!, method: .get, parameters: nil).responseJSON { response in
var arrayIndexes = [IndexPath]()
switch(response.result) {
case .success(_):
let data = response.result.value as! [String : Any]
if let responseData = Mapper<DataModel>().map(JSON: data) {
if responseData.results!.count > 0{
self.arrayExploreStylistList = []
}
for i in 0..<responseData.results!.count{
arrayIndexes.append(IndexPath(row: self.arrayExploreStylistList.count + i, section: 0))
}
self.arrayExploreStylistList.append(contentsOf: responseData.results!)
print(arrayIndexes.count)
}
// if let arrNew = data["results"] as? [[String : Any]]{
// let jobData = Mapper<DataModel>().mapArray(JSONArray: arrNew)
// print(jobData)
// self.datamodel = jobData
// }
self.tblView.reloadData()
break
case .failure(_):
print(response.result.error as Any)
break
}
}
}
}
I'm attempting to use SwiftyJson to pull some JSON data.
What's unusual is the "println(json)" says "unknowon" while if I pull the JSON data the regular way it works just fine -- the "println(pop)" says medium, as expected.
Below is the code I'm using. I started cutting out parts until I got to "println(json)" and then decided to try and handle it manually to see if it's SwiftyJson or me.
Any suggestions? I'm fairly new to iOS programming so I'm assuming I'm being silly in some form or another.
var ghostlandsJsonUrl: NSURL = NSURL(string: "http://us.battle.net/api/wow/realm/status?realm=Ghostlands")!
var jsonData: NSData!
var request: NSURLRequest = NSURLRequest(URL: ghostlandsJsonUrl)
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config)
let task : NSURLSessionDataTask = session.dataTaskWithRequest(request, completionHandler: {(data, response, error) in
jsonData = data
if(jsonData != nil) {
let json = JSON(jsonData)
println(json)
} else {
println("jsonData: nil value... net down again?")
}
let jsonObject : AnyObject! = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: nil)
if let statuses = jsonObject as? NSDictionary{
if let realms = statuses["realms"] as? NSArray{
if let realm = realms[0] as? NSDictionary{
if let pop = realm["population"] as? NSString{
println(pop)
}
}
}
}
});
task.resume()
Looking at SwiftyJSON source code I can see that JSON is a simple struct. It implements the Printable protocol. Which give support to the print methods.
public var description: String {
if let string = self.rawString(options:.PrettyPrinted) {
return string
} else {
return "unknown"
}
}
Which means that for a reason or another the rawString method returns nil.
public func rawString(encoding: UInt = NSUTF8StringEncoding, options opt: NSJSONWritingOptions = .PrettyPrinted) -> String? {
switch self.type {
case .Array, .Dictionary:
if let data = self.rawData(options: opt) {
return NSString(data: data, encoding: encoding)
} else {
return nil
}
case .String:
return (self.object as String)
case .Number:
return (self.object as NSNumber).stringValue
case .Bool:
return (self.object as Bool).description
case .Null:
return "null"
default:
return nil
}
}
As you are fairly new to iOS development, I will tell you that the constructor doesn't expect a NSData object.
Here is the source:
public var object: AnyObject {
get {
return _object
}
set {
_object = newValue
switch newValue {
case let number as NSNumber:
if number.isBool {
_type = .Bool
} else {
_type = .Number
}
case let string as NSString:
_type = .String
case let null as NSNull:
_type = .Null
case let array as [AnyObject]:
_type = .Array
case let dictionary as [String : AnyObject]:
_type = .Dictionary
default:
_type = .Unknown
_object = NSNull()
_error = NSError(domain: ErrorDomain, code: ErrorUnsupportedType, userInfo: [NSLocalizedDescriptionKey: "It is a unsupported type"])
}
}
}
So you should pass it the unserialized NSData as it:
if let jsonData = data {
//jsonData can't be nil with this kind of if
let jsonObject : AnyObject! = NSJSONSerialization.JSONObjectWithData(jsonData, options: NSJSONReadingOptions.MutableContainers, error: nil)
let json = JSON(jsonObject)
println(json)
//...
The constructor of JSON does the serialisation. Below is the constructor code from SwiftyJSON git repo where you can directly pass the NSData.
public init(data:NSData, options opt: NSJSONReadingOptions = .AllowFragments, error: NSErrorPointer = nil) {
do {
let object: AnyObject = try NSJSONSerialization.JSONObjectWithData(data, options: opt)
self.init(object)
} catch let aError as NSError {
if error != nil {
error.memory = aError
}
self.init(NSNull())
}
}
In simple, you can directly use the data returned in the completion handler of NSURLSession data task as below in your code.
let json = JSON(data: jsonData)