A question from Implementing Codable for UIColor
struct WrapperOfNSCoding<Wrapped>: Codable where Wrapped: NSCoding {
var wrapped: Wrapped
init(_ wrapped: Wrapped) { self.wrapped = wrapped }
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let data = try container.decode(Data.self)
guard let object = NSKeyedUnarchiver.unarchiveObject(with: data) else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "failed to unarchive an object")
}
guard let wrapped = object as? Wrapped else {
throw DecodingError.typeMismatch(Wrapped.self, DecodingError.Context(codingPath: container.codingPath, debugDescription: "unarchived object type was \(type(of: object))"))
}
self.wrapped = wrapped
}
func encode(to encoder: Encoder) throws {
let data = NSKeyedArchiver.archivedData(withRootObject: wrapped)
var container = try encoder.singleValueContainer()
try container.encode(data)
}
}
let colors = [NSColor.red, NSColor.brown]
print(colors)
let w = WrapperOfNSCoding(colors[0])
let jsonData = try! JSONEncoder().encode(w) - fails
let jsonData = try! JSONEncoder().encode(colors.map({ WrapperOfNSCoding($0) })) - succeeds
print(jsonData)
let colors2 = try! JSONDecoder().decode([WrapperOfNSCoding<NSColor>].self, from: jsonData).map({ $0.wrapped })
print(colors2)
The error is when a single value is used in the encoder
let w = WrapperOfNSCoding(colors[0])
let jsonData = try! JSONEncoder().encode(w)
error is
Fatal error: 'try!' expression unexpectedly raised an error:
Swift.EncodingError.invalidValue(WrapperOfNSCoding #1...
This succeeds
let w = WrapperOfNSCoding([colors[0]])
let jsonData = try! JSONEncoder().encode(w)
Why would that be
JSONEncoder need valid JSON context on the top level, which could be either [:] (Dictionary) or [] (Array), inside you can place an element like in this example string.
When you save any NSCoding object, the compiler treats that object as the string. In the case of JSONEncoder().encode(w), you are trying to encode an NSCoding object which acts as a string object instead of regular JSON object.
In the case of JSONEncoder().encode([w]), the object has created an array and each element has a string which is NSCoding in your case.
In another way w data is a string and [w] is an array with each index is having a string, therefore JSONEncoder is not giving you an error with [w] and giving you an error for w.
Related
I'm totally new with swift, it's my first iOs app
I would like to retrieve a value from an http POST response
struct represCode: Codable{
var CODE: String?
}
var table = [represCode]()
func httpPost(completion: #escaping (_ json: Any?)->()) {
let json: [String: Any] = ["login": usernameText.text!.uppercased(),
"pass": mdpText.text!]
let urlPath = url.chaine + "login.php"
let jsonData = try? JSONSerialization.data(withJSONObject: json)
let url = URL(string: urlPath)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
// insert json data to the request
request.httpBody = jsonData
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else {
print(error?.localizedDescription ?? "No data")
return
}
do {
self.table = try JSONDecoder().decode([represCode].self, from: data)
print(self.table)
self.dl = true
}catch _ {
print ("JSON Error")
}
completion(json)
}
task.resume()
}
When I "print(self.table)" I get this
[Mobois.LoginViewController.represCode(CODE: Optional("AG"))]
And I would like to store the "AG" in a specific var (ex: var represCode: String?)
I tried many solutions that I found here but most of time I get errors like "Cannot assign value of type '[LoginViewController.represCode]' to type 'String'"
There are two serious mistakes.
The root object is an array (represented by the [] in [represCode].self)
The value AG is the value for key CODE
First of all to conform to the naming convention declare the struct this way
struct RepresCode: Decodable {
let code: String
private enum CodingKeys: String, CodingKey { case code = "CODE" }
}
and
var table = [RepresCode]()
..
JSONDecoder().decode([RepresCode].self ...
You can access the value by getting the value for property code of the first item in the array
let represCode = table.first?.code ?? "unknown code"
This is the main struct that encodes properly as far as when I print print(String(data: encoded, encoding: .utf8) as Any)
the struct prints encoded with mainObject printing first then the rest of the variables in the struct but i want it to print: mainAccount, mainObject, reference in that order.
struct T: Codable {
init(){}
let mainAccount = "IMHOTECHPECOM"
let mainObject = mainObject()
let reference = UUID()
// Enum that allows easy encoding
enum CodingKeys: CodingKey {
case mainAccount, mainObject, reference;
}
// function to conform to encodable protocol
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(mainAccount, forKey: .merchantAccount)
try container.encode(mainObject.self, forKey: .mainObject)
try container.encode(reference, forKey: .reference)
}
// conforms with decodable protocol
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
_ = try container.decode(String.self, forKey: .mainAccount)
_ = try container.decode(String.self, forKey: .mainObject)
_ = try container.decode(String.self, forKey: .reference)
}
}
This is the mainObject
struct mainObject: Codable {
var type = "kind"
var identifier: String = ""
var guide: String = ""
init(){}
enum CodingKeys: CodingKey{
case type, identifier, guide;
}
// function to conform to encodable protocol
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(type, forKey: .type)
try container.encode(identifier, forKey: .identifier)
try container.encode(guide, forKey: .guide)
}
// conforms with decodable protocol
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
type = try container.decode(String.self, forKey: .type)
identifier = try container.decode(String.self, forKey: .identifier)
guide = try container.decode(String.self, forKey: .identifier)
}
}
This is the function in the actual view encoding the data from a button press
func getBalance() async {
let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys]
encoder.outputFormatting = [.withoutEscapingSlashes]
encoder.outputFormatting = [.prettyPrinted]
guard let encoded = try? encoder.encode(mainobject) else {
print("Failed to encode")
return
}
let url = URL(string: "https://testurl.com")!
var request = URLRequest(url: url)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
do {
let (response, _) = try await URLSession.shared.upload(for: request, from: encoded)
print(String(data: encoded, encoding: .utf8) as Any)
print(String(data: response, encoding: .utf8) as Any)
} catch {
print("Encoding failed")
}
let _ = try? JSONDecoder().decode(mainObject.self, from: encoded)
In this particular case, the order you're asking for happens to match alphabetical order. So if you add .sortedKeys to your encoder's outputFormatting property, you'll get the order you want:
let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys]
let data = try encoder.encode(myT)
This will affect the order of keys in all objects in the JSON. So your T's keys will be output in the order mainAccount, mainObject, reference, and your mainObject's keys will be output in the order guide, identifier, type.
The general answer is that JSONEncoder doesn't remember the order in which you add keys to a keyed container. Internally, it uses a standard Swift Dictionary to store the keys and values. A Swift Dictionary doesn't guarantee any ordering of its keys, and the order can change each time your program is started.
If you want to guarantee that the order of your keys is preserved, you'll have to write your own Encoder implementation, which is not a trivial task.
I am trying to decode the error as follows, most of the error that I am handling in array format [String], but in few cases the error is not in array format, just a String.
If error comes in array format name comes as errors, but if it is string format then it comes as error. How could I handle this scenario?
How could I able to handle this scenario?
struct CustomError: Codable {
let errors: [String]
}
private func errorDecoding(data : Data) {
let decoder = JSONDecoder()
do {
let errorData = try decoder.decode(CustomError.self, from: data)
} catch {
// TODO
}
}
You'd have to manually implement init(from:) and try decoding one type, failing that, decode another:
struct CustomError {
let errors: [String]
}
extension CustomError: Decodable {
enum CodingKeys: CodingKey { case errors, error }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
self.errors = try container.decode([String].self, forKey: .errors)
} catch DecodingError.typeMismatch,
DecodingError.keyNotFound {
let error = try container.decode(String.self, forKey: .error)
self.errors = [error]
}
}
}
The decoding part is normal:
do {
let error = try JSONDecoder().decode(CustomError.self, from: data)
} catch {
// ..
}
I am attempting to decode a JSON response from a third-party API which contains nested/child JSON that has been base64 encoded.
Contrived Example JSON
{
"id": 1234,
"attributes": "eyAibmFtZSI6ICJzb21lLXZhbHVlIiB9",
}
PS "eyAibmFtZSI6ICJzb21lLXZhbHVlIiB9" is { 'name': 'some-value' } base64 encoded.
I have some code that is able to decode this at present but unfortunately I have to reinstanciate an additional JSONDecoder() inside of the init in order to do so, and this is not cool...
Contrived Example Code
struct Attributes: Decodable {
let name: String
}
struct Model: Decodable {
let id: Int64
let attributes: Attributes
private enum CodingKeys: String, CodingKey {
case id
case attributes
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int64.self, forKey: .id)
let encodedAttributesString = try container.decode(String.self, forKey: .attributes)
guard let attributesData = Data(base64Encoded: encodedAttributesString) else {
fatalError()
}
// HERE IS WHERE I NEED HELP
self.attributes = try JSONDecoder().decode(Attributes.self, from: attributesData)
}
}
Is there anyway to achieve the decoding without instanciating the additional JSONDecoder?
PS: I have no control over the response format and it cannot be changed.
If attributes contains only one key value pair this is the simple solution.
It decodes the base64 encoded string directly as Data – this is possible with the .base64 data decoding strategy – and deserializes it with traditional JSONSerialization. The value is assigned to a member name in the Model struct.
If the base64 encoded string cannot be decoded a DecodingError will be thrown
let jsonString = """
{
"id": 1234,
"attributes": "eyAibmFtZSI6ICJzb21lLXZhbHVlIiB9",
}
"""
struct Model: Decodable {
let id: Int64
let name: String
private enum CodingKeys: String, CodingKey {
case id, attributes
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int64.self, forKey: .id)
let attributeData = try container.decode(Data.self, forKey: .attributes)
guard let attributes = try JSONSerialization.jsonObject(with: attributeData) as? [String:String],
let attributeName = attributes["name"] else { throw DecodingError.dataCorruptedError(forKey: .attributes, in: container, debugDescription: "Attributes isn't eiter a dicionary or has no key name") }
self.name = attributeName
}
}
let data = Data(jsonString.utf8)
do {
let decoder = JSONDecoder()
decoder.dataDecodingStrategy = .base64
let result = try decoder.decode(Model.self, from: data)
print(result)
} catch {
print(error)
}
I find the question interesting, so here is a possible solution which would be to give the main decoder an additional one in its userInfo:
extension CodingUserInfoKey {
static let additionalDecoder = CodingUserInfoKey(rawValue: "AdditionalDecoder")!
}
var decoder = JSONDecoder()
let additionalDecoder = JSONDecoder() //here you can put the same one, you can add different options, same ones, etc.
decoder.userInfo = [CodingUserInfoKey.additionalDecoder: additionalDecoder]
Because the main method we use from JSONDecoder() is func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable and I wanted to keep it as such, I created a protocol:
protocol BasicDecoder {
func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable
}
extension JSONDecoder: BasicDecoder {}
And I made JSONDecoder respects it (and since it already does...)
Now, to play a little and check what could be done, I created a custom one, in the idea of having like you said a XML Decoder, it's basic, and it's just for the fun (ie: do no replicate this at home ^^):
struct CustomWithJSONSerialization: BasicDecoder {
func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable {
guard let dict = try JSONSerialization.jsonObject(with: data) as? [String: Any] else { fatalError() }
return Attributes(name: dict["name"] as! String) as! T
}
}
So, init(from:):
guard let attributesData = Data(base64Encoded: encodedAttributesString) else { fatalError() }
guard let additionalDecoder = decoder.userInfo[.additionalDecoder] as? BasicDecoder else { fatalError() }
self.attributes = try additionalDecoder.decode(Attributes.self, from: attributesData)
Let's try it now!
var decoder = JSONDecoder()
let additionalDecoder = JSONDecoder()
decoder.userInfo = [CodingUserInfoKey.additionalDecoder: additionalDecoder]
var decoder2 = JSONDecoder()
let additionalDecoder2 = CustomWithJSONSerialization()
decoder2.userInfo = [CodingUserInfoKey.additionalDecoder: additionalDecoder]
let jsonStr = """
{
"id": 1234,
"attributes": "eyAibmFtZSI6ICJzb21lLXZhbHVlIiB9",
}
"""
let jsonData = jsonStr.data(using: .utf8)!
do {
let value = try decoder.decode(Model.self, from: jsonData)
print("1: \(value)")
let value2 = try decoder2.decode(Model.self, from: jsonData)
print("2: \(value2)")
}
catch {
print("Error: \(error)")
}
Output:
$> 1: Model(id: 1234, attributes: Quick.Attributes(name: "some-value"))
$> 2: Model(id: 1234, attributes: Quick.Attributes(name: "some-value"))
After reading this interesting post, I came up with a reusable solution.
You can create a new NestedJSONDecodable protocol which gets also the JSONDecoder in it's initializer:
protocol NestedJSONDecodable: Decodable {
init(from decoder: Decoder, using nestedDecoder: JSONDecoder) throws
}
Implement the decoder extraction technique (from the aforementioned post) together with a new decode(_:from:) function for decoding NestedJSONDecodable types:
protocol DecoderExtractable {
func decoder(for data: Data) throws -> Decoder
}
extension JSONDecoder: DecoderExtractable {
struct DecoderExtractor: Decodable {
let decoder: Decoder
init(from decoder: Decoder) throws {
self.decoder = decoder
}
}
func decoder(for data: Data) throws -> Decoder {
return try decode(DecoderExtractor.self, from: data).decoder
}
func decode<T: NestedJSONDecodable>(_ type: T.Type, from data: Data) throws -> T {
return try T(from: try decoder(for: data), using: self)
}
}
And change your Model struct to conform to NestedJSONDecodable protocol instead of Decodable:
struct Model: NestedJSONDecodable {
let id: Int64
let attributes: Attributes
private enum CodingKeys: String, CodingKey {
case id
case attributes
}
init(from decoder: Decoder, using nestedDecoder: JSONDecoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int64.self, forKey: .id)
let attributesData = try container.decode(Data.self, forKey: .attributes)
self.attributes = try nestedDecoder.decode(Attributes.self, from: attributesData)
}
}
The rest of your code will remain the same.
You could create a single decoder as a static property of Model, configure it once, and use it for all your Model decoding needs, both externally and internally.
Unsolicited thought:
Honestly, I would only recommend doing that if you're seeing a measurable loss of CPU time or crazy heap growth from the allocation of additional JSONDecoders… they're not heavyweight objects, less than 128 bytes unless there's some trickery I don't understand (which is pretty common though tbh):
let decoder = JSONDecoder()
malloc_size(Unmanaged.passRetained(decoder).toOpaque()) // 128
I'm using the new Decodable protocol introduced in Swift 4.
Inside my unit test, I want to use a generic method that decodes a specific JSON file for a specific Decodable type.
I wrote the following function matching the JSONDecoder decode method:
var jsonDecoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return decoder
}()
static let bundle: Bundle = {
let testBundle = Bundle(for: Decodable.self)
let sampleURL = testBundle.url(forResource: "api_samples", withExtension: "bundle")!
return Bundle(url: sampleURL)!
}()
static func getJSONSample(fileName: String) throws -> Data {
let url = Decodable.bundle.url(forResource: fileName, withExtension: "json")!
return try Data(contentsOf: url)
}
func assertDecode<Obj>(_ type: Obj.Type, fileName: String) where Obj: Decodable {
do {
let data = try Decodable.getJSONSample(fileName: fileName)
let _ = try jsonDecoder.decode(type, from: data)
// Same by using Obj.self, Obj.Type
} catch let error {
XCTFail("Should not have failed for \(type) with json \(fileName): \(error)")
}
}
The compiler gives me the following error:
In argument type 'Obj.Type', 'Obj' does not conform to expected type 'Decodable'
I would have imagine that Obj is decodable due to the where clause.
What is wrong with that function?
Instead of doing a "where" statement, make your life easier by restricting the generic itself:
func assertDecode<Obj: Decodable>(_ type: Obj.Type, fileName: String)