In Swift, how to serialize an arbitrary object in Toml format? - swift

Thanks for your help. I need interaction with Toml files in my macOS Swift application. I am using the TOMLDecoder library to parse the Toml format. The library works by specifying a Swift struct type that conforms to Codable, and have the library create the object for us. From the docs:
struct Discography: Codable {
struct Album: Codable {
let name: String
struct Song: Codable {
let name: String
}
let songs: [Song]
}
let albums: [Album]
}
If we take a sample Toml file:
[[albums]]
name = "Born to Run"
[[albums.songs]]
name = "Jungleland"
[[albums.songs]]
name = "Meeting Across the River"
[[albums]]
name = "Born in the USA"
[[albums.songs]]
name = "Glory Days"
[[albums.songs]]
name = "Dancing in the Dark"
We can parse it with:
let tomlData = try? Data(contentsOf: URL(fileURLWithPath: "/path/to/file"))
let discography = try? TOMLDecoder().decode(Discography.self, from: tomlData)
Here comes my question. The library does not provide a way to reverse the process, so to serialize back the object, so I would like to write that on my own, and, possibly, I would like to achieve a solution in clean Swift, if I understand correctly, by the use of the T type, thus allowing any kind of Codable conforming object to be serializable. The decode function in the library is:
public func decode<T: Decodable>(_ type: T.Type, from text: String) throws -> T {
let topLevel: Any
do {
topLevel = try TOMLDeserializer.tomlTable(with: text)
} catch {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid TOML.", underlyingError: error))
}
let decoder = TOMLDecoderImpl(referencing: self, options: self.options)
guard let value = try decoder.unbox(topLevel, as: type) else {
throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: [], debugDescription: "The given data did not contain a top-level value."))
}
return value
}
I have started to write my encode function like the following:
class TOMLEncoder: TOMLDecoder {
func encode<T>(sourceObject: T) -> String {
return "Sample serialized text..."
}
}
I really don't know how to proceed... from my very limited knowledge I should iterate somehow on the sourceObject properties and create the TOML file from the contents of those properties, but I am not sure if that is the correct approach and how to achieve it. Any help is greatly appreciated. Thanks

Related

Error: "Expected to decode Dictionary<String, Any> but found an array instead." — but I haven't defined a dictionary? [duplicate]

This question already has answers here:
Decoding Error -- Expected to decode Dictionary<String, Any> but found an array instead
(2 answers)
Closed 1 year ago.
I'm working on a creative project, and I'm trying to decode content from an API database using Swift's JSONDecoder() function. I've built my structs, a getData() function, and I've set up a do-try-catch for the JSONDecoder() function. I'm having difficulty understanding what I'm doing to get the error I'm getting.
Here are my structs:
struct Response: Codable {
let foundRecipes: [Recipe]
let foundIngredients: [Ingredient]
}
struct Recipe: Codable {
let id: Int
let title: String
let image: String
let imageType: String
let usedIngredientCount: Int
let missedIngredientCount: Int
let missedIngredients: [Ingredient]
let usedIngredients: [Ingredient]
let unusedIngredients: [Ingredient]
let likes: Int
}
struct Ingredient: Codable {
let id: Int
let amount: Int
let unit: String
let unitLong: String
let unitShort: String
let aisle: String
let name: String
let original: String
let originalString: String
let origianalName: String
let metaInformation: [String]
let meta: [String]
let image: String
}
Here's my getData() function:
func getData(from url: String) {
URLSession.shared.dataTask(with: URL(string: url)!, completionHandler: { data, response, error in
guard let data = data, error == nil else {
print("something went wrong.")
return
}
var result: Response?
do {
result = try JSONDecoder().decode(Response.self, from: data)
}
catch {
print("")
print(String(describing: error)) // Right here is where the error hits.
}
guard let json = result else {
return
}
print(json.foundRecipes)
}).resume()
}
Here's a link to the API's documentation. The URL I'm calling in getData() links to the same structure of search as shown in their example: https://spoonacular.com/food-api/docs#Search-Recipes-by-Ingredients — and here's a screenshot of the url results for the exact search I'm working on: https://imgur.com/a/K3Rn9SZ
And finally, here's the full error that I'm catching:
typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Dictionary<String, Any> but found an array instead.", underlyingError: nil))
My understanding of this error is that it's saying I told the JSONDecoder() to look for a Dictionary of <String, Any>, but it's at the link and only seeing an array. I'm confused, because I don't know where it thinks I'm providing a dictionary. Where am I screwing up? Not looking for specific code changes, just some guidance on what I'm missing.
Thanks in advance :)
As you can see in your image of the API data and in the API documentation you linked to, the API is returning an array (in the documentation, for example, you can see that it is surrounded by [...]). In fact, it looks like the API returns an array of Recipe.
So, you can change your decoding call to this:
var result: [Recipe]?
do {
result = try JSONDecoder().decode([Recipe].self, from: data)
print(result)
} catch {
print(error)
}
Perhaps your idea for Response came from somewhere else, but the keys foundRecipes or foundIngredients don't show up in this particular API call.
Also, thanks to #workingdog's for a useful comment about changing amount to a Double instead of an Int in your model.

Can't unarchive custom Object with NSKeyedUnarchiver in AR Multiuser

I'm using MultipeerConnectivity to share SCNNodes position in a multiuser AR session.
When I archive (with NSKeyedArchiver.archivedData(withRootObject: someARNode, requiringSecureCoding: true) )
And unarchive (with if let node = try NSKeyedUnarchiver.unarchivedObject(ofClass:SCNNode.self, from: data) {)
Everything works fine, but now I'm trying to send a custom Object like this:
struct ARUser: Codable {
var idUser : String
var position: [Double]
}
When I try to unarchive the object received with the NSKeyedUnarchiver.unarchivedObject it let me error.
if let node = try NSKeyedUnarchiver.unarchivedObject(ofClass:ARUser.self, from: data) {...}
I get the syntax error: Incorrect argument label in call (have 'ofClass:from:', expected 'ofClasses:from:')
But if I change the function as suggested by the compiler:
if let node = try NSKeyedUnarchiver.unarchivedObject(ofClasses:[ARUser.self], from: data) {..}
I get the next syntax error: Cannot convert value of type 'ARUser.Type' to expected element type 'AnyObject.Type'
So, the question here is, what's the correct way to unarchive custom Objects?
Since here you use Codable
struct ARUser: Codable {
Then
do {
let dataToObject = try JSONDecoder().decode(ARUser.self,from:data)
let objectToData = try JSONEncoder().encode(dataToObject)
}
catch {
print(error)
}
NSKeyedUnarchiver is an old Objective-C stuff

Casting from Any to anything else fails

API gives me back a variable that has type Any. It looks like this when I print it.
{
"sender" : "Kira",
"created" : "08.05.2018",
"text" : "Cncncm"
}
I tried to use SwiftyJSON to cast it like this let mydata = JSON(data) but it failes. I tried to use Swift 4 decoding technique but that failed as well. I tried to do this let myData = data as? Dictionary<String, String> but it fails again.
I am clueless what to do here. Any tips or solutions?
Finally a chance to demonstrate one of the Codable protocols hidden gems. Please run the following in a Playground:
import Cocoa
let jsonData = """
{
"sender" : "Kira",
"created" : "08.05.2018",
"text" : "Cncncm"
}
""".data(using: .utf8)!
struct SenderText: Codable {
let sender: String
let created: Date
let text: String
}
let dayFormatter = DateFormatter()
dayFormatter.dateFormat = "dd.MM.yyyy"
let date = dayFormatter.date(from:"08.05.2018")
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(dayFormatter)
do {
let sendText = try decoder.decode(SenderText.self, from: jsonData)
print(sendText)
} catch {
print(error)
}
The sheer elegance of how easy it is to define such an intricate parser mapping a messy JSON-string to your favourite struct will hardly ever stop to amaze me. No matter how weird your date format looks, it is hardly more than 3 lines away from being parsed during the process.
There is something in regard to casting you should note though: In Swift, as in most object oriented languages, you can only cast something to something else if (and only if) it already is something else in the first place (but that knowledge has been lost somewhere). Since your String is "just" a String (in disguise of an Any maybe) you won't be able to cast it to anything else. However the Codable protocol provides you with a terrific means to decode from the Strings Data with astonishing ease. This process should not be mistaken as a cast, even if it looks largely the same. It is the creation and initialisation of another, more fittingly structured object from a simple piece of Data that you are likely to have gotten from your average web service of choice.
Great so far, at least in my book.
You can parse it like this as it's a json string
let trd = yourVar as? String
if let data = trd?.data(using: String.Encoding.utf8) {
do {
var content = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as! [String:String]
print(content)
}
catch let error as NSError {
print(error)
}
}

Alamofire XML request to PropertyList

I am trying to parse an XML data using Codable from the sample https://www.w3schools.com/xml/note.xml.
My struct is
struct Note: Codable {
var to: String?
var from: String?
var heading: String?
var body: String?
}
However if I make the following request I get the error responseSerializationFailed : ResponseSerializationFailureReason "PropertyList could not be serialized because of error:\nThe data couldn’t be read because it isn’t in the correct format."
let url = URL(string: "https://www.w3schools.com/xml/note.xml")
Alamofire.request(url!, method: .get, encoding: PropertyListEncoding.default).responsePropertyList { (response) in
guard response.error == nil else {
print(response.error!)
exp.fulfill()
return
}
print(response)
if let data = response.data {
print(data)
let decoder = PropertyListDecoder()
let note = try! decoder.decode(Note.self, from: data)
print(note)
}
}
How do you exactly work with the responsePropertyList in Alamofire?
Currently, Apple's Codable protocol does not have a way to decode XML. While a Plist is XML, XML is not necessarily a Plist unless it follows a certain format.
While there are plenty of third party libraries, I would suggest you take a look at the XMLParsing library. This library contains a XMLDecoder and a XMLEncoder that uses Apple's own Codable protocol, and is based on Apple's JSONEncoder/JSONDecoder with changes to fit the XML standard.
Link: https://github.com/ShawnMoore/XMLParsing
W3School's XML To Parse:
<note>
<to>Tove</to>
<from>Jani</from>
<heading>Reminder</heading>
<body>Don't forget me this weekend!</body>
</note>
Swift Struct conforming to Codable:
struct Note: Codable {
var to: String
var from: String
var heading: String
var body: String
}
XMLDecoder:
let data = Data(forResource: "note", withExtension: "xml") else { return nil }
let decoder = XMLDecoder()
do {
let note = try decoder.decode(Note.self, from: data)
} catch {
print(error)
}
XMLEncoder:
let encoder = XMLEncoder()
do {
let data = try encoder.encode(self, withRootKey: "note")
print(String(data: data, encoding: .utf8))
} catch {
print(error)
}
There are a number of benefits for using Apple's Codable protocol over that of a third-party's protocol. Take for example if Apple decides to begin supporting XML, you would not have to refactor.
For a full list of examples of this library, see the Sample XML folder in the repository.
There are a few differences between Apple's Decoders and Encoders to fit the XML standard. These are as follows:
Differences between XMLDecoder and JSONDecoder
XMLDecoder.DateDecodingStrategy has an extra case titled keyFormatted. This case takes a closure that gives you a CodingKey, and it is up to you to provide the correct DateFormatter for the provided key. This is simply a convenience case on the DateDecodingStrategy of JSONDecoder.
XMLDecoder.DataDecodingStrategy has an extra case titled keyFormatted. This case takes a closure that gives you a CodingKey, and it is up to you to provide the correct data or nil for the provided key. This is simply a convenience case on the DataDecodingStrategy of JSONDecoder.
If the object conforming to the Codable protocol has an array, and the XML being parsed does not contain the array element, XMLDecoder will assign an empty array to the attribute. This is because the XML standard says if the XML does not contain the attribute, that could mean that there are zero of those elements.
Differences between XMLEncoder and JSONEncoder
Contains an option called StringEncodingStrategy, this enum has two options, deferredToString and cdata. The deferredToString option is default and will encode strings as simple strings. If cdata is selected, all strings will be encoded as CData.
The encode function takes in two additional parameters than JSONEncoder does. The first additional parameter in the function is a RootKey string that will have the entire XML wrapped in an element named that key. This parameter is required. The second parameter is an XMLHeader, which is an optional parameter that can take the version, encoding strategy and standalone status, if you want to include this information in the encoded xml.
PropertyList files although are in XML format, they need to follow Apple's PropertyList DTD: http://www.apple.com/DTDs/PropertyList-1.0.dtd
If you want to map a regular XML file (that do not follow PropertyList DTD) into a model object and you don't mind using an external library you can try XMLMapper.
You model for this XML should look like this:
class Note: XMLMappable {
var nodeName: String!
var to: String?
var from: String?
var heading: String?
var body: String?
required init(map: XMLMap) { }
func mapping(map: XMLMap) {
to <- map["to"]
from <- map["from"]
heading <- map["heading"]
body <- map["body"]
}
}
And you can map it from string using XMLMapper:
let note = XMLMapper<Note>().map(XMLString: xmlString)
Or if you install Requests subspec you can use responseXMLObject(queue:keyPath:mapToObject:completionHandler:) function like:
let url = URL(string: "https://www.w3schools.com/xml/note.xml")
Alamofire.request(url!, method: .get, encoding: XMLEncoding.default).responseXMLObject { (response: DataResponse<Note>) in
let note = response.result.value
print(note?.from ?? "nil")
}
Hope this helps.

Swift as overload

I am creating simple Json Parser that works like that: I have JsonData class that contains Anyobject as data. When I use jsonData["key"] it returns JsonData to i can chain jsonData["key"]["key2"] etc.
My question is how can I implement that class so i could cast it to lets say String:
jsonData["key"] as String without using some workarouds like
jsonData["key"].data as String
Code:
class JsonData:CustomStringConvertible{
let data:AnyObject
var description: String{
get{
return "\(data)"
}
}
init(_ data: Data) {
self.data = try! JSONSerialization.jsonObject(with: data, options: []) as! [[String:AnyObject]]
}
init(_ data: AnyObject) {
self.data = data
}
subscript(key:String) -> JsonData{
let newData = data as! [String:AnyObject]
let test = newData[key]!
return JsonData(test)
}
subscript(index:Int) ->JsonData{
let newData = data[index]!
return JsonData(newData)
}
}
In order to do this, you'd add another overload, but it won't work like you're thinking.
subscript(key: String) -> String {
let newData = data as! [String:AnyObject]
return newData[key] as! String
}
So then jsonData["key"] as String works, but jsonData["key"]["key2"] is ambiguous and you'd have to write it (jsonData["key"] as JsonData)["key2"] which probably isn't what you want.
The short answer to this is don't do this. If you need this much access to JSON, you're probably storing your data incorrectly. Parse it to structs as quickly as you can, and then work with structs. Convert the structs back to JSON when you want that. Extensive work with AnyObject is going to break your brain and the compiler over and over again. AnyObject is a necessary evil, not an every day tool. Soon you will encounter that terrible day that you have an AnyObject? and the compiler just breaks down in tears. Well, at least it isn't Any.
Putting that aside, the better solution is to use labeled-subscripts.
subscript(string key: String) -> String {
let newData = data as! [String:AnyObject]
return newData[key] as! String
}
Now you can access that as json[string: "key"] rather than json["key"] as String.