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.
Related
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
I'm fairly new to this. Anyway, here we go:
I have JSON data that comes from an API. For the sake of this question, I have simplified it greatly. You can run the following code in a Playground.
import UIKit
struct Book: Codable {
let image: String
}
// this comes from my API
let jsonString = "{ \"image\" = \"someURL\" }"
print(jsonString) // { "image" = "someURL" }
// convert String to Data
let jsonData = jsonString.data(using: .utf8)
// decode data (in my project, I catch the error, of course)
let decoder = JSONDecoder()
let decodingResult = try? decoder.decode(Book.self, from: jsonData!)
print(decodingResult) // nil
As you can see, I'm trying to decode my JSON-String into an Object (my Struct), but the Decoder always returns nil.
Can someone point me in the right direction?
Thank you.
Your current jsonString isn't a proper JSON. Change it to "{ \"image\": \"someURL\" }", and it should work. For more information on JSON syntax, check this manual.
I am attempting to decode my JSON response data from type: AnyObject? back into something that can be printed out in the console / interacted with.
reading back the data, before decoding prints projectName.GameData
Here is the breakdown, data comes back from the response as type: Any? Because it sent up as
class GameData : Codable {
var isPlayerOneTurn: Bool!
var wasCreated: Bool!
var playerOne: String!
var playerTwo: String!
var board: [[Int]]!
init() {
}
}
The current error I am getting when attempting to decode is Cannot convert value of type 'GameData' to expected argument type 'Data'
code :
let decoder = JSONDecoder()
let dataTest = try? decoder.decode(GameData.self, from: data)
Am I missing a correct init() method on the GameData class?
UPDATE:
data was changed to type Data here: thank you #rmaddy for the comment pointing this out.
let data = data as? Data
let decoder = JSONDecoder()
let dataTest = try? decoder.decode(GameData.self, from: data!)
print("data: \(String(describing: dataTest))")
the print line still shows data: Optional(projectName.GameData)
What is wrong here still, not allowing me to view the values of the class GameData?
The print line mentioned in the question, was the value of the game object decode.. That was all XCode would print out - the name of the original object before decoding. Using dataTest.myValue worked when accessing data from the object.
According to the JSON standard RFC 7159, this is valid json:
22
How do I decode this into an Int using swift4's decodable? This does not work
let twentyTwo = try? JSONDecoder().decode(Int.self, from: "22".data(using: .utf8)!)
It works with good ol' JSONSerialization and the .allowFragments
reading option. From the documentation:
allowFragments
Specifies that the parser should allow top-level objects that are not an instance of NSArray or NSDictionary.
Example:
let json = "22".data(using: .utf8)!
if let value = (try? JSONSerialization.jsonObject(with: json, options: .allowFragments)) as? Int {
print(value) // 22
}
However, JSONDecoder has no such option and does not accept top-level
objects which are not arrays or dictionaries. One can see in the
source code that the decode() method calls
JSONSerialization.jsonObject() without any option:
open func decode<T : Decodable>(_ type: T.Type, from data: Data) throws -> T {
let topLevel: Any
do {
topLevel = try JSONSerialization.jsonObject(with: data)
} catch {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: error))
}
// ...
return value
}
In iOS 13.1+ and macOS 10.15.1+ JSONDecoder can handle primitive types on root level.
See the latest comments (Oct 2019) in the linked article underneath Martin's answer.
I am trying to scrape some data from a website, and since there is no API, I am trying to use ALAMOFIRE + KANNA
I can print my results in the console, but as soon as I try to convert in String to use it in my app it says:
Could not cast value of type 'Kanna.libxmlHTMLNode' (0x10887d210) to 'NSString' (0x108efc0d0).
Why couldn't I cast the data in String using as! String
my code
var competitions:[String] = []
// Grabs the HTML
func scrapeData() -> Void {
Alamofire.request("MYWEBSITE.com").responseString { response in
print("\(response.result.isSuccess)")
if let html = response.result.value {
self.parseHTML(html: html)
}
}
}
func parseHTML(html: String) -> Void {
if let doc = try? Kanna.HTML(html: html, encoding: String.Encoding.utf8) {
do {
// Search for nodes by XPATH selector
for competition in doc.xpath("""
//*[#id="page_teams_1_block_teams_index_club_teams_2"]/ul
""") {
let competitionName = competition.at_xpath("li/div/a")
print(competitionName?.content ?? "N/A")
competitions.append(competition as! String)
competition is a libxmlHTMLNode, not a String. You can't simply force-cast one type of object to another, unrelated type.
Most likely you want to append competitionName, not competition to your string array. But you need to convert it to a String using its text property:
competitions.append(competitionName?.text ?? "N/A")