I have seen a piece of code online which uses Swift Codable do decode a JSON into a struct.
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let stringValue = try? container.decode(String.self, forKey: .someId), let value = Int64(stringValue) {
someId = value
} else {
someId = try container.decode(Int64.self, forKey: .someId)
}
}
This code:
decodes a string
tries to parse it into Int64
if it fails - it directly attempts to decode an Int64
My question is - is this code superfluous?
Is there any scenario where Int64.init(_:) from String would be able to decode something that JSONDecoder.decode wouldn't?
And actually, isn't this "decode String - init Int64" the exact same thing that JSONDecoder does under the hood?
It is not superfluous. It could be used to handle JSON that sometimes has numbers encoded as a string (within quotes), and sometimes just as a number.
For example, the JSON could sometimes be:
{
"someId": "12345"
}
in which case you need to decode to String, and then Int64.init
And sometimes the JSON could be:
{
"someId": 12345
}
in which case decoding to String would fail, and you would directly decode to Int64.
Related
What I want to do:
I want to get an array from UserDefaults that I saved beforehand and append a custom object to it. Afterwards I want to encode it as a Data-type again and set this as the UserDefaults Key again.
My problem:
The encoding part is what is not working as intended for me.
It says: -[__SwiftValue encodeWithCoder:]: unrecognized selector sent to instance 0x60000011a540
But I do not know how to fix this.
Below is my code for more context:
do {
let decoded = defaults.object(forKey: "ExArray") as! Data
var exo = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(decoded) as! [Exerc]
exo.append(datas[indexPath.row])
let enco = try NSKeyedArchiver.archivedData(withRootObject: exo, requiringSecureCoding: false) <- Here is the error
defaults.set(enco, forKey: "ExArray")
} catch {
print("Error encoding custom object NOSEARCHO")
}
This is how Exerc looks:
struct Exerc: Codable {
var title: String
var exID: String
}
Seems like you are not using the archiver features, so why don't you just use the codable?
do {
let key = "ExArray"
let decoded = defaults.data(forKey: key)!
var exo = try JSONDecoder().decode([Exerc].self, from: decoded)
exo.append(datas[indexPath.row])
let enco = try JSONEncoder().encode(exo)
defaults.set(enco, forKey: key)
} catch {
print("Error encoding/decoding custom object NOSEARCHO", error)
}
It just a simple refactored MVP of the original code, but you can even work a bit on this and make it human readable right in the plist file!
I have types for which I provide CodingKeys with custom names for their data members as appropriate. I would like to encode, as required by different call sites, only a subset of a type's CodingKeys before sending the data to a server. The required encodings change from call site to call site, so there's no default implementation to consider.
Example types (for demonstration purposes):
struct Account: Codable
{
let name: String;
var balance: Float;
mutating func set(balance: Float)
{
self.balance = balance; // verifications, etc.
}
}
struct User: Codable
{
enum CodingKeys: String, CodingKey
{
case
id = "db_id",
name = "db_name",
account
// many more keys
}
let id: Int;
let name: String;
var account: Account;
// many more data members
}
Creating an instance:
var user = User(
id: 1, name: "john", account: Account(name: "checking", balance: 10_000));
Using a JSONEncoder works as expected; it produces the following:
{
"db_id" : 1,
"db_name" : "john",
"account" : {
"balance" : 10000,
"name" : "checking"
}
}
I want to encode a subset of the User type in order to send that data back to a server so that I can update specific data fields instead of updating the entire set of properties of my type. Mock usage example:
user.account.set(balance: 15_000);
let jsonEncoding = JSONEncodeSubset.of(
user, // instance to encode
keys: [User.CodingKeys.id, User.CodingKeys.account] // data to include
);
The resulting produced JSON would look like so:
{
"db_id" : 1,
"account" : {
"balance" : 15000,
"name" : "checking"
}
}
Server side, we now have the exact data we need to perform our desired update.
Another example: somebody entered the wrong name for our user, therefore another update request looks like this:
user.name = "jon"; // assume the model was modified to make this mutable
let jsonEncoding = JSONEncodeSubset.of(
user, // instance to encode
keys: [User.CodingKeys.id, User.CodingKeys.name] // only encode id & name
);
The expected resulting JSON encoding:
{
"db_id" : 1,
"name" : "jon"
}
Note that we exclude the information that isn't part of the update (user's account). The objective is to optimize the encoding to include only data that is relevant to that specific request.
Considering I have a large list of objects to update, with different call sites updating different things, I'd like to have a succinct way to perform the encoding task.
Encoding a subset of a type's data members improves encoding performance/memory footprint.
Sending a larger count of smaller encoded objects to the server becomes more efficient.
Does Swift provide any such support for encoding subsets of a type's CodingKeys?
Expanding on Joakim Danielson's answer you can use CodingUserInfoKey to pass data to userInfo property of JSONEncoder.
extension CodingUserInfoKey {
static let keysToEncode = CodingUserInfoKey(rawValue: "keysToEncode")!
}
extension JSONEncoder {
func withEncodeSubset<CodingKeys>(keysToEncode: [CodingKeys]) -> JSONEncoder {
userInfo[.keysToEncode] = keysToEncode
return self
}
}
You need to make CodingKeys conform to CaseIterable and implement a custom encode(to:) method:
struct User: Codable {
enum CodingKeys: String, CodingKey, CaseIterable { // add `CaseIterable`
...
}
...
func encode(to encoder: Encoder) throws {
let keysToEncode = encoder.userInfo[.keysToEncode] as? [CodingKeys] ?? CodingKeys.allCases
var container = encoder.container(keyedBy: CodingKeys.self)
for key in keysToEncode {
switch key {
case .id:
try container.encode(id, forKey: .id)
case .account:
try container.encode(account, forKey: .account)
case .name:
try container.encode(name, forKey: .name)
}
}
}
}
And use it like this:
let encoder = JSONEncoder().withEncodeSubset(keysToEncode: [User.CodingKeys.id, .account])
let encoded = try encoder.encode(user)
print(String(data: encoded, encoding: .utf8)!)
// prints: {"db_id":1,"account":{"name":"checking","balance":15000}}
This solution requires that you write a custom encode(to:) for all properties but once that is done it should be easy to use.
The idea is to let the encode(to:) encode only those properties/keys that exists in a given array. So given the example above the function would look like this
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
for key in selectedCodingKeys {
switch key {
case .id:
try container.encode(id, forKey: .id)
case .account:
try container.encode(account, forKey: .account)
case .name:
try container.encode(name, forKey: .name)
}
}
}
selectedCodingKeys is new property in the struct
var selectedCodingKeys = [CodingKeys]()
and we could also add a specific function for encoding
mutating func encode(for codingKeys: [CodingKeys]) throws -> Data {
self.selectedCodingKeys = codingKeys
return try JSONEncoder().encode(self)
}
and then the decoding could be done like in this example
var user = User(id: 1, name: "John", account: Account(name: "main", balance: 100.0))
do {
let data1 = try user.encode(for: [.id, .account])
let data2 = try user.encode(for: [.id, .name])
} catch {
print(error)
}
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)
}
}
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.
I am parsing a response that needs to be transformed from a dictionary on the server (which is a legacy data format) - to simply an array of strings on the client side. Therefore I am wanting to decode the key called 'data' as a dictionary, so i can iterate through the keys and create an array of strings on the client side.
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
do {
let some_data_dictionary = try values.decode([String:Any].self, forKey: CodingKeys.data)
for (kind, values) in some_data_dictionary {
self.data_array.append(kind)
}
} catch {
print("we could not get 'data' as [String:Any] in legacy data \(error.localizedDescription)")
}
}
The error I am getting is: Ambiguous reference to member 'decode(_:forKey:)'
Looks like Swift 'Codable' cant support Any or use of [String:Any], so using this post here Swift 4 decodable nested json with random key attributes
I was able to make a struct for a class I wouldn't use called LegacyData, and then unpack the keys into an array of strings
do
{
let legacy_data = try values.decode([String:LegacyData].self, forKey: CodingKeys.data)
self.array = Array(legacy_data.keys)
}
catch
{
print("no legacy_data \(error) \n")
}