Codable and Xmlparser in Swift

Codable and XMLParser in Swift

Currently, Apple's Codable protocol does not have a way to decode XML.

While there are plenty of third party libraries to parse XML, the XMLParsing 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

  1. 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.
  2. 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.
  3. 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

  1. 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.

  2. 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.

swift make XML using codingkeys

To use your CodingKey in the serialization process, you probably want to use the new Codable protocol, but Foundation does not support XML.

Shawn Moore wrote an interesting library that adds the missing, from the Foundation, XMLDecoder and XMLEncoder classes. (Codable support for XML) Although it does not have a readme file, this answer can help you about how to use it.

Another solution is to try XMLMapper. This library uses the same idea as ObjectMapper but for XML.

For example you can serialize the following model struct:

struct Person: XMLMappable {
var nodeName: String! = "Person"

var id: String?
var nameOfBoss: String?

init() {

}

init(map: XMLMap) {

}

mutating func mapping(map: XMLMap) {
id <- map["id"]
nameOfBoss <- map["nam_of_boss"]
}
}

To the following XML:

<Person>
<id>1</id>
<nam_of_boss>Greg</nam_of_boss>
</Person>

Using toXMLString() function of the XMLMappable protocol:

var person = Person()
person.nameOfBoss = "Greg"
person.id = "1"
let xmlString = person.toXMLString()

Hope this helps.

How to decode xml with xmlcoder?

Okay finally decode it after just changing the case text = "text" with this

struct GUID: Codable {
let isPermaLink: String
let text: String

enum CodingKeys: String, CodingKey {
case isPermaLink = "isPermaLink"
case text = ""
}
}

Many thanks to @Larme

Custom XML Parser - what to do when some keys don't exist?

In the XMLParsing library, you will need to make the property an optional if it might not exist in the data.

This is similar to the behavior you will find in Apple's JSONDecoder if the value doesn't exist in the JSON.


In your example, you mentioned pubDate might not exist in your XML, so the updated channel structure would appear as the following:

struct RSSChannel: Codable {
var title: String
var pubDate: Date?
var link: URL
var description: String
var items: [RSSItem]

enum CodingKeys: String, CodingKey {
case title, pubDate, link, description
case items = "item"
}
}

So in the cases where pubDate does exist in your XML, there will be a date in that property. In the cases that the key does not exist in the XML, the value for pubDate will be nil.

You can make as many properties as you want in your structure optional and they will all follow this pattern.

Implementing a custom Decoder in Swift 4

I haven't had a chance to turn my code into a framework yet, but you can take a look at my Github Repository that implements both a custom decoder and encoder for XML.

Link: https://github.com/ShawnMoore/XMLParsing

The encoder and decoder resides in the XML folder of the repo. It is based on Apple's JSONEncoder and JSONDecoder with changes to fit the XML standard.


Differences between XMLDecoder and JSONDecoder

  1. 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.
  2. 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.
  3. 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

  1. 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.

  2. 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.



Examples

For a full list of examples, see the Sample XML folder in the repository.

XML To Parse:

<?xml version="1.0"?>
<book id="bk101">
<author>Gambardella, Matthew</author>
<title>XML Developer's Guide</title>
<genre>Computer</genre>
<price>44.95</price>
<publish_date>2000-10-01</publish_date>
<description>An in-depth look at creating applications
with XML.</description>
</book>

Swift Structs:

struct Book: Codable {
var id: String
var author: String
var title: String
var genre: Genre
var price: Double
var publishDate: Date
var description: String

enum CodingKeys: String, CodingKey {
case id, author, title, genre, price, description

case publishDate = "publish_date"
}
}

enum Genre: String, Codable {
case computer = "Computer"
case fantasy = "Fantasy"
case romance = "Romance"
case horror = "Horror"
case sciFi = "Science Fiction"
}

XMLDecoder:

let data = Data(forResource: "book", withExtension: "xml") else { return nil }

let decoder = XMLDecoder()

let formatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
return formatter
}()

decoder.dateDecodingStrategy = .formatted(formatter)

do {
let book = try decoder.decode(Book.self, from: data)
} catch {
print(error)
}

XMLEncoder:

let encoder = XMLEncoder()

let formatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
return formatter
}()

encoder.dateEncodingStrategy = .formatted(formatter)

do {
let data = try encoder.encode(self, withRootKey: "book", header: XMLHeader(version: 1.0))

print(String(data: data, encoding: .utf8))
} catch {
print(error)
}

iOS Generic type for codable property in Swift

T must also conform to Codable

struct BaseJsonStruct<T : Codable> : Codable {
let info: String
let data: T
}

What is difference between optional and decodeIfPresent when using Decodable for JSON Parsing?

There's a subtle, but important difference between these two lines of code:

// Exhibit 1
foo = try container.decode(Int?.self, forKey: .foo)
// Exhibit 2
foo = try container.decodeIfPresent(Int.self, forKey: .foo)

Exhibit 1 will parse:

{
"foo": null,
"bar": "something"
}

but not:

{
"bar": "something"
}

while exhibit 2 will happily parse both. So in normal use cases for JSON parsers you'll want decodeIfPresent for every optional in your model.



Related Topics



Leave a reply



Submit