I created a struct and want to save it as a JSON-file.
struct Sentence {
var sentence = ""
var lang = ""
}
var s = Sentence()
s.sentence = "Hello world"
s.lang = "en"
print(s)
...which results in:
Sentence(sentence: "Hello world", lang: "en")
But how can I convert the struct object to something like:
{
"sentence": "Hello world",
"lang": "en"
}
Swift 4 introduces the Codable protocol which provides a very convenient way to encode and decode custom structs.
struct Sentence : Codable {
let sentence : String
let lang : String
}
let sentences = [Sentence(sentence: "Hello world", lang: "en"),
Sentence(sentence: "Hallo Welt", lang: "de")]
do {
let jsonData = try JSONEncoder().encode(sentences)
let jsonString = String(data: jsonData, encoding: .utf8)!
print(jsonString) // [{"sentence":"Hello world","lang":"en"},{"sentence":"Hallo Welt","lang":"de"}]
// and decode it back
let decodedSentences = try JSONDecoder().decode([Sentence].self, from: jsonData)
print(decodedSentences)
} catch { print(error) }
Swift 4 supports the Encodable protocol e.g.
struct Sentence: Encodable {
var sentence: String?
var lang: String?
}
let sentence = Sentence(sentence: "Hello world", lang: "en")
Now you can automatically convert your Struct into JSON using a JSONEncoder:
let jsonEncoder = JSONEncoder()
let jsonData = try jsonEncoder.encode(sentence)
Print it out:
let jsonString = String(data: jsonData, encoding: .utf8)
print(jsonString)
{
"sentence": "Hello world",
"lang": "en"
}
https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types
Use the NSJSONSerialization class.
Using this for reference, you may need to create a function which returns the JSON serialized string. In this function you could take the required properties and create a NSDictionary from them and use the class mentioned above.
Something like this:
struct Sentence {
var sentence = ""
var lang = ""
func toJSON() -> String? {
let props = ["Sentence": self.sentence, "lang": lang]
do {
let jsonData = try NSJSONSerialization.dataWithJSONObject(props,
options: .PrettyPrinted)
return String(data: jsonData, encoding: NSUTF8StringEncoding)
} catch let error {
print("error converting to json: \(error)")
return nil
}
}
}
Because your struct only has two properties it might be easier to just build the JSON string yourself.
Here's a nice extension and a method for JSON encoding/decoding:
extension Encodable {
func toJSONString() -> String {
let jsonData = try! JSONEncoder().encode(self)
return String(data: jsonData, encoding: .utf8)!
}
}
func instantiate<T: Decodable>(jsonString: String) -> T? {
return try? JSONDecoder().decode(T.self, from: jsonString.data(using: .utf8)!)
}
Sample usage:
struct Sentence: Codable {
var sentence = ""
var lang = ""
}
let sentence = Sentence(sentence: "Hello world", lang: "en")
let jsonStr = sentence.toJSONString()
print(jsonStr) // prints {"lang":"en","sentence":"Hello world"}
let sentenceFromJSON: Sentence? = instantiate(jsonString: jsonStr)
print(sentenceFromJSON!) // same as original sentence
Related
I try to convert a JSON to an Array but I face some issue and do not know how to sort it out.
I use Swift 5 and Xcode 12.2.
Here is the JSON from my PHP query:
[
{
Crewcode = AAA;
Phone = 5553216789;
Firstname = Philip;
Lastname = MILLER;
email = "pmiller#xxx.com";
},
{
Crewcode = BBB;
Phone = 5557861243;
Firstname = Andrew;
Lastname = DEAN;
email = "adean#xxx.com";
}
]
And here is my Swift code :
let url: URL = URL(string: "https://xxx.php")!
let defaultSession = Foundation.URLSession(configuration: URLSessionConfiguration.default)
let task = defaultSession.dataTask(with: url) { (data, response, error) in
if error != nil {
print("Failed to download data")
}
else {
print("Data downloaded")
do {
if let jsondata = (try? JSONSerialization.jsonObject(with: data!, options: [])) {
print(jsondata)
struct Crew: Decodable {
var Code: String
var Phone: String
var Firstname: String
var Lastname: String
var email: String
}
let decoder = JSONDecoder()
do {
let people = try decoder.decode([Crew].self, from: jsondata as! Data)
print(people)
}
catch {
print(error)
}
}
}
}
}
task.resume()
When I run my code I get the following error:
Could not cast value of type '__NSArrayI' (0x7fff86b930b0) to 'NSData' (0x7fff86b911e8).
2020-12-09 14:52:48.988468+0100 FTL[57659:3019805] Could not cast value of type '__NSArrayI' (0x7fff86b930b0) to 'NSData' (0x7fff86b911e8).
Could not cast value of type '__NSArrayI' (0x7fff86b930b0) to 'NSData' (0x7fff86b911e8).
Should you have any idea to get it right, I thank you in advance for your assistance on this !
You are deserializing the JSON twice by mixing up JSONSerialization and JSONDecoder.
Delete the first one
if let jsondata = (try? JSONSerialization.jsonObject(with: data!, options: [])) {
– By the way the JSON in the question is neither fish nor fowl, neither JSON nor an NS.. collection type dump –
and replace
let people = try decoder.decode([Crew].self, from: jsondata as! Data)
with
let people = try decoder.decode([Crew].self, from: data!)
and the struct member names must match the keys otherwise you have to add CodingKeys
I am trying to create a basic to-do application for command-line in Swift. Below is my function to add a new item to the to-do array, but the new entry keeps overwriting the old one, instead of creating a new one. In the end, there is only one entry in the todo.json file.
When I multiply the entries and .append statements manually it works, but probably my brain is too dead to figure it out at the moment.
struct TodoItem: Codable {
let name: String
}
var todoList = [TodoItem]()
func addToList(_ item: String) -> String {
let todoItem = TodoItem(name: item)
todoList.append(todoItem)
do {
let fileURL = try FileManager.default
.url(for: .applicationSupportDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: true)
.appendingPathComponent("example")
.appendingPathExtension("json")
let encoder = JSONEncoder()
try encoder.encode(todoList).write(to: fileURL)
} catch {
return "Error: \(error.localizedDescription)"
}
return "Item added: \(todoItem.name)"
}
Your code works fine. I think the problem rests in the fact that todoList is empty when you run this. But you can write code to retrieve the contents of the JSON:
var todoList: [TodoItem] = []
func retrieveToDoList() {
guard let data = try? Data(contentsOf: fileURL) else { return }
todoList = (try? JSONDecoder().decode([TodoItem].self, from: data)) ?? []
}
So, for example, consider:
retrieveToDoList()
addToList("foo")
addToList("bar")
addToList("baz")
print(todoList)
// if you want to check the json
let data = try! Data(contentsOf: fileURL)
let json = String(data: data, encoding: .utf8)!
print(json)
That results in:
[MyApp.TodoItem(name: "foo"), MyApp.TodoItem(name: "bar"), MyApp.TodoItem(name: "baz")]
[
{
"name" : "foo"
},
{
"name" : "bar"
},
{
"name" : "baz"
}
]
And if I later do:
addToList("abc")
addToList("def")
addToList("hij")
I then get:
[
{
"name" : "foo"
},
{
"name" : "bar"
},
{
"name" : "baz"
},
{
"name" : "abc"
},
{
"name" : "def"
},
{
"name" : "hij"
}
]
So, every time the app starts up, just make sure to call retrieveToDoList before trying to append items or else the toDoList will be empty.
FYI, this is the code I used to generate the above. Hopefully it illustrates the idea.
struct TodoItem: Codable {
let name: String
}
class ViewController: UIViewController {
private var todoList: [TodoItem] = []
private let fileURL = try! FileManager.default
.url(for: .applicationSupportDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: true)
.appendingPathComponent("example.json")
override func viewDidLoad() {
super.viewDidLoad()
retrieveToDoList()
addToList("foo")
addToList("bar")
addToList("baz")
print(todoList)
// if you want to check the json
let data = try! Data(contentsOf: fileURL)
let json = String(data: data, encoding: .utf8)!
print(json)
}
}
private extension ViewController {
func retrieveToDoList() {
guard let data = try? Data(contentsOf: fileURL) else { return }
todoList = (try? JSONDecoder().decode([TodoItem].self, from: data)) ?? []
}
#discardableResult
func addToList(_ item: String) -> String {
let todoItem = TodoItem(name: item)
todoList.append(todoItem)
do {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
try encoder.encode(todoList).write(to: fileURL)
} catch {
return "Error: \(error.localizedDescription)"
}
return "Item added: \(todoItem.name)"
}
}
I'm using an API that returns this pretty horrible JSON:
[
"A string",
[
"A string",
"A string",
"A string",
"A string",
…
]
]
I'm trying to decode the nested array using JSONDecoder, but it doesn't have a single key and I really don't know where to start… Do you have any idea?
Thanks a lot!
If the structure stays the same, you can use this Decodable approach.
First create a decodable Model like this:
struct MyModel: Decodable {
let firstString: String
let stringArray: [String]
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
firstString = try container.decode(String.self)
stringArray = try container.decode([String].self)
}
}
Or if you really want to keep the JSON's structure, like this:
struct MyModel: Decodable {
let array: [Any]
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
let firstString = try container.decode(String.self)
let stringArray = try container.decode([String].self)
array = [firstString, stringArray]
}
}
And use it like this
let jsonString = """
["A string1", ["A string2", "A string3", "A string4", "A string5"]]
"""
if let jsonData = jsonString.data(using: .utf8) {
let myModel = try? JSONDecoder().decode(MyModel.self, from: jsonData)
}
This is a bit interesting for decoding.
You don't have any key. So it eliminates the need of a wrapper struct.
But look at the inner types. You get mixture of String and [String] types. So you need something that deals with this mixture type. You would need an enum to be precise.
// I've provided the Encodable & Decodable both with Codable for clarity. You obviously can omit the implementation for Encodable
enum StringOrArrayType: Codable {
case string(String)
case array([String])
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
self = try .string(container.decode(String.self))
} catch DecodingError.typeMismatch {
do {
self = try .array(container.decode([String].self))
} catch DecodingError.typeMismatch {
throw DecodingError.typeMismatch(StringOrArrayType.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Encoded payload conflicts with expected type"))
}
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .string(let string):
try container.encode(string)
case .array(let array):
try container.encode(array)
}
}
}
Decoding Process:
let json = """
[
"A string",
[
"A string",
"A string",
"A string",
"A string"
]
]
""".data(using: .utf8)!
do {
let response = try JSONDecoder().decode([StringOrArrayType].self, from: json)
// Here, you have your Array
print(response) // ["A string", ["A string", "A string", "A string", "A string"]]
// If you want to get elements from this Array, you might do something like below
response.forEach({ (element) in
if case .string(let string) = element {
print(string) // "A string"
}
if case .array(let array) = element {
print(array) // ["A string", "A string", "A string", "A string"]
}
})
} catch {
print(error)
}
A possible solution is to use the JSONSerialization, then you might simply dig inside such json, doing so:
import Foundation
let jsonString = "[\"A string\",[\"A string\",\"A string\", \"A string\", \"A string\"]]"
if let jsonData = jsonString.data(using: .utf8) {
if let jsonArray = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [Any] {
jsonArray.forEach {
if let innerArray = $0 as? [Any] {
print(innerArray) // this is the stuff you need
}
}
}
}
I want to easily decode a JSON file with Decode protocol from Swift 4 on Xcode 9. This my question :
How to decode à JSON like this:
[
{
"name": "My first Catalog",
"order": 0,
"products": [
{
"product": {
"title": "product title",
"reference": "ref"
}
}
]
}
]
I try this but it's doesn't work
fileprivate struct Catalog: Codable {
var name: String
var order: Int
var product: [Product]
}
fileprivate struct Product: Codable {
var title: String
var reference: String
}
...
// JSON Decoder
do {
let jsonData = try Data(contentsOf: URL(fileURLWithPath: filePath), options: .alwaysMapped)
let jsonDecoder = JSONDecoder()
let jsonCatalogs = try? jsonDecoder.decode(Array<Catalog>.self,
from: jsonData)
return jsonCatalogs
} catch {
print ("error")
return nil
}
I don't know why it doesn't work in Swift 4 with Xcode 9. Thank for your help ;-)
actually the thing is that your structs are wrong,
fileprivate struct Catalog: Codable {
var name: String
var order: Int
var products: [Products] // this key is wrong in your question, it should be products instead of product
}
//this particular structure was missing as your products is having a dictionary and in that dictionary you are having product at key product
fileprivate struct Products: Codable {
var product: Product
}
fileprivate struct Product: Codable {
var title: String
var reference: String
}
now you can check your fucntion and also you can easily debug it using try catch with error handlings
...
// JSON Decoder
do {
let jsonData = try Data(contentsOf: URL(fileURLWithPath:filePath), options: .alwaysMapped)
let jsonDecoder = JSONDecoder()
let jsonCatalogs = try? jsonDecoder.decode(Array<Catalog>.self,from: jsonData)
print(jsonCatalogs)
return jsonCatalogs
} catch let error {
print ("error -> \(error)") // this will always give you the exact reason for which you are getting error
return nil
}
Try this solution according to your JSON and i have worked with Decoder in swift 4 with xcode 9.4.
struct Catalog : Decodable {
let name : String?
let order : Int?
let productArray : [Products]? // this is products array.
}
struct Products : Decodable {
let productDict : Product? // this is product dictionary
}
struct Product : Decodable {
let title : String?
let reference : String?
}
var catalogArray = [Catalog]() // Declaration
// JSON Decoder
do {
let jsonData = try Data(contentsOf: URL(fileURLWithPath:filePath), options: .alwaysMapped)
let jsonDecoder = JSONDecoder()
let jsonCatalogs = try? jsonDecoder.decode(Catalog.self,from: jsonData)
return jsonCatalogs
} catch let error {
print ("error -> \(error)") // this will always give you the exact reason for which you are getting error
return nil
}
*
I have a problem to make a JSON from an array of struct in Swift3. I searched in Stack Overflow, nothing help me (here the screenshot). I have a struct like this:
public struct ProductObject {
var prodID: String
var prodName: String
var prodPrice: String
var imageURL: String
var qty: Int
var stock: String
var weight: String
init(prodID: String, prodName: String, prodPrice: String, imageURL: String, qty: Int, stock: String, weight: String){
self.prodID = prodID
self.prodName = prodName
self.prodPrice = prodPrice
self.imageURL = imageURL
self.qty = qty
self.stock = stock
self.weight = weight
}
}
and the array of that struct:
private var productsArray = [ProductObject]()
When the array is not empty, and then I tried to print it in another class, it shows this in debugger:
[app.cartclass.ProductObject(prodID: "2", prodName: "produk 2", prodPrice: "IDR 1000000", imageURL: "someURL", qty: 1, stock: "11", weight: "200")]
The array is not a valid JSON object. How to make it a valid JSON object? And I wonder whether this part "app.cartclass.ProductObject" is a problem or not to make it a valid JSON object?
edit:
Here's how I serialize into a JSON:
var products = [String:Any]()
for j in 0 ..< cart.numberOfItemsInCart() {
products=["\(j)":cart.getAllProduct(atIndex: j)]
}
if let valid = JSONSerialization.isValidJSONObject(products) {
do {
let jsonproducts = try JSONSerialization.data(withJSONObject: products, options: .prettyPrinted) as! [String:Any]
//print(jsonproducts)
} catch let error as NSError {
print(error)
}
} else {
print("it is not a valid JSON object");
}
If you want to make JSON from custom object then first you need to convert your custom object to Dictionary, so make one function like below in your ProductObject struct.
func convertToDictionary() -> [String : Any] {
let dic: [String: Any] = ["prodID":self.prodID, "prodName":self.prodName, "prodPrice":self.prodPrice, "imageURL":self.imageURL, "qty":qty, "stock":stock, "weight":weight]
return dic
}
Now use this function to generate Array of dictionary from Array of custom object ProductObject.
private var productsArray = [ProductObject]()
let dicArray = productsArray.map { $0.convertToDictionary() }
Here dicArray is made of type [[String:Any]], now you can use JSONSerialization to generate JSON string from this dicArray.
if let data = try? JSONSerialization.data(withJSONObject: dicArray, options: .prettyPrinted) {
let str = String(bytes: data, encoding: .utf8)
print(str)
}