How to encode Dictionary with JSONEncoder in Swift 4 - swift

I want to encode Dictionary to json with JSONEncoder.
It seems like a Request, receive a dictionary as parameter and encode it to json as http body.
The code is looks like this:
let dict = ["name": "abcde"]
protocol Request {
var params: [String: Encodable] { get set }
func encode<T>(_ value: T) throws -> Data where T : Encodable
}
extension Request {
func encode<T>(_ value: T) throws -> Data where T : Encodable {
return try JSONEncoder().encode(value)
}
var body: Data? {
if let encoded = try? self.encode(self.params) {
return encoded
}
return nil
}
}
struct BaseRequest: Request {
var params: [String : Encodable]
}
let req = BaseRequest(params: dict)
let body = req.body
But this code occurs error
Fatal error: Dictionary<String, Encodable> does not conform to Encodable because Encodable does not conform to itself. You must use a concrete type to encode or decode.
How could I make this encodable?

You have to introduce type erasure as follows:
struct AnyEncodable: Encodable {
let value: Encodable
init(value: Encodable) {
self.value = value
}
func encode(to encoder: Encoder) throws {
try value.encode(to: encoder)
}
}
struct Model: Encodable {
var params: [String: AnyEncodable]
}
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let json = try! encoder.encode(
Model(
params: [
"hello" : AnyEncodable.init(value: "world")
]
).params
)
print(String(data: json, encoding: .utf8))

If you want to define your struct as conforming to Codable, you can do it like this:
struct Model: Codable {
var param1: String
var param2: Int
}
let model = Model(param1: "test", param2: 0)
let encoded = try? JSONEncoder().encode(model)
let decoded = try? JSONDecoder().decode(Model.self, from: encoded!)
It won't really work if you set params: [String: Any] because the encoders/decoders don't know how to encode/decode Any, but they can do it for the primitive types.
If you want more help, you should read more about the new Codable protocol. I recommend this: https://hackernoon.com/everything-about-codable-in-swift-4-97d0e18a2999

Related

Issue Decoding a Set for Tableview

I am using the following struct that conforms to Codable to represent my data within a Set, I then take this set encode it into a JSON Array that looks like the following [{"myexample":"Example5","id":3}]
Struct and Encoding:
struct Model : Codable, Hashable {
var myexample: String?
var id: Int?
}
var mySet: Set<Model> = []
mySet.insert(Model(myexample: "Example4", id: 3))
do {
let json = try JSONEncoder().encode(mySet)
print(String(data: json, encoding: .utf8)!)
} catch {
print(error)
}
How can I inversely decode this array using the following function?
Currently getting this error:
Value of type 'Set' has no member 'utf8'
func processArray(array:Set<Model> = []) {
do{
let mydata = Data(array.utf8.data)
let decoded = try JSONDecoder().decode([Model].self,from: mydata)
print(decoded)
}catch let jsonErr {
print(jsonErr)
}
}
JSON is a string format so a JSON array is also string
func processJSON(_ json: String) {
do{
let mydata = Data(json.utf8)
let decoded = try JSONDecoder().decode(Set<Model>.self,from: mydata)
print(decoded)
} catch {
print(error)
}
}

How do I pass a custom Codable Model into a Firebase Function?

What's the best way to pass a custom Codable Model into a firebase function? Here's what I have right now, but I get the error: Unsupported type: __SwiftValue for value
struct ExampleStruct: Codable {
var hi: String
var world: Int
var last: [String]
}
let myModel = ExampleStruct(hi: "World", world: 1, last: ["hi world"])
functions.httpsCallable(FirebaseFunctionsNames.exampleName).call(["data":myModel]) { [weak self] (result, error) in
print(result)
print(error)
}
The following ended up solving my question (note: I wouldn't recommend the force unwraps and casts but it's easier to consume in StackOverflow this way :)
struct ExampleStruct: Codable {
var hi: String
var world: Int
var last: [String]
}
let myModel = ExampleStruct(hi: "World", world: 1, last: ["hi world"])
func hiWorld(input: myModel) {
let jsonEncoder = JSONEncoder()
let jsonData = try! jsonEncoder.encode(input)
let json = String(data: jsonData, encoding: .utf8)!
functions.httpsCallable(FirebaseFunctionsNames.exampleName).call(["data":json]) { [weak self] (result, error) in
print(result)
print(error)
}
}
And then on the backend (Typescript):
const myModel = JSON.parse(data.data) as ExampleStruct;

generic swift object serialization?

Is there a generic way to serialize/deserialize objects for iOS? I was using the following code, and the system functions I was calling were deprecated in iOS 12:
func object(forKey:String) -> Any? {
if let data = get(BLOB_COL, forKey) {
return NSKeyedUnarchiver.unarchiveObject(with: data as! Data)
}
return nil
}
func set(_ object:Any, forKey: String) {
let data = NSKeyedArchiver.archivedData(withRootObject: object)
updateOrInsert(forKey, BLOB_COL, data)
}
It looks like the new versions of these functions requires knowledge of the object classes, and different signatures for unarchiving different collection types... is there a simple way to handle this generically?
If you would like to use NSKeyedArchiver/NSKeyedUnarchiver:
When archiving change:
NSKeyedArchiver.archivedData...
to
NSKeyedArchiver.archivedData(withRootObject: object, requiringSecureCoding: false)
And when unarchiving change
NSKeyedUnarchiver.unarchiveObject...
to
NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data)
Codable protocol generic approach
If you would like to use Codable to encode and decode (serialize) your Data you can extend Encodable protocol and create a method to encode any object that conforms to it:
extension Encodable {
func data(using encoder: JSONEncoder = JSONEncoder()) throws -> Data { try encoder.encode(self) }
}
And to decode the encoded data you can extend Data and create a generic method that decode any object that conforms to Decodable:
extension Data {
func object<T: Decodable>(using decoder: JSONDecoder = JSONDecoder()) throws -> T {
try decoder.decode(T.self, from: self)
}
// you can also create a string property to convert the JSON data to String
var string: String? { String(data: self, encoding: .utf8) }
}
Usage:
struct Person: Codable {
let name: String
let age: Int
}
do {
// encoding
let person = Person(name: "Joe", age: 10)
var people = [Person]()
people.append(person)
let data = try people.data()
print(data.string ?? "") // [{"name":"Joe","age":10}]
// decoding
let loadedPeople: [Person] = try data.object()
loadedPeople.forEach({print( $0.name, $0.age)}) // Joe 10
} catch {
print(error)
}
As Leo says, stop using NSKeyedArchiver and use the Codable protocol. That's the modern, Swifty way to serialize/deserialize your objects.

How can I make a Decodable object from a dictionary?

I want to have a Struct that can be instantiated via normal Codable protocol or a Dictionary (Existing code requires the Dictionary instantiation).
I have this code in a playground, but I'm not sure what to do in my 2nd init that takes a Dictionary. How do I make a Decoder object from a dictionary?
import Foundation
public protocol Parsable: Decodable {
init(dict: [String: Any]) throws
}
struct LinkModel: Parsable {
var href: String
var text: String
init(dict: [String: Any]) throws {
href = "/store/options.aspx"
text = "Buy"
}
}
struct ResponseModel: Parsable {
var link: LinkModel?
let showCell: Bool
enum CodingKeys : String, CodingKey {
case link
case showCell = "show"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let linkResponses = try container.decode([LinkModel].self, forKey: .link)
link = linkResponses.first
showCell = try container.decode(Bool.self, forKey: .showCell)
}
init(dict: [String: Any]) throws {
let jsonData = try JSONSerialization.data(withJSONObject: dict, options: [])
let decoder = ??? // what do I do here?
self.init(from: decoder)
}
}
let jsonText = """
{
"show": true,
"link": [
{
"text": "Buy",
"href": "/store/options.aspx"
}
]
}
"""
// test standard Decodable instantiation
let jsonData = jsonText.data(using: .utf8)!
let model = try! JSONDecoder().decode(ResponseModel.self, from: jsonData)
print(model.link?.href)
// test dictionary instantiation
...
Extend your Parsable protocol to automatically generate the initializer you're looking for.
extension Parsable {
init(dict: [String: Any]) throws {
let jsonData = try JSONSerialization.data(withJSONObject: dict, options: [])
let decoder = JSONDecoder()
self = try decoder.decode(Self.self, from: jsonData)
}
}
You're on the right path.
import Foundation
public protocol Parsable: Decodable {
init(dict: [String: Any]) throws
}
struct LinkModel: Parsable {
var href: String
var text: String
init(dict: [String: Any]) throws {
href = "/store/options.aspx"
text = "Buy"
}
}
struct ResponseModel: Parsable {
var link: LinkModel?
let showCell: Bool
enum CodingKeys : String, CodingKey {
case link
case showCell = "show"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let linkResponses = try container.decode([LinkModel].self, forKey: .link)
link = linkResponses.first
showCell = try container.decode(Bool.self, forKey: .showCell)
}
init(dict: [String: Any]) throws {
let jsonData = try JSONSerialization.data(withJSONObject: dict, options: [])
// 1.
let decoder = JSONDecoder()
// 2.
let result = try decoder.decode(ResponseModel.self, from: jsonData)
// 3.
self = result
}
}
let jsonText = """
{
"show": true,
"link": [
{
"text": "Buy",
"href": "/store/options.aspx"
}
]
}
"""
// test standard Decodable instantiation
let jsonData = jsonText.data(using: .utf8)!
let model = try! JSONDecoder().decode(ResponseModel.self, from: jsonData)
print(model.link?.href)
All I did was:
create a JSONdecoder object.
use that JSONdecoder to decode an object of type ResponseModel
assign the result of the decoding to self. This way all properties of self are assigned.

Dictionary extension in Swift is not recognized

I am trying to extend Dictionary with the following code:
extension Dictionary where Key: ExpressibleByStringLiteral, Value: AnyObject {
var jsonString: String? {
if let dict = (self as AnyObject) as? Dictionary<String, AnyObject> {
do {
let data = try JSONSerialization.data(withJSONObject: dict, options: JSONSerialization.WritingOptions(rawValue: UInt.allZeros))
if let string = String(data: data, encoding: String.Encoding.utf8) {
return string
}
} catch {
print(error)
}
}
return nil
}
}
Where I write something like:
let x: [String: String] = ["": ""]
x.jsonString
I get this error:
Value of type '[String: String]' as no member 'jsonString'
Anything I am missing?
There is no need to constrain Dictionary Value type at all:
extension Dictionary where Key: ExpressibleByStringLiteral {
var jsonString: String? {
guard let data = try? JSONSerialization.data(withJSONObject: self)
else { return nil }
return String(data: data, encoding: .utf8)
}
}
Since String is a value type , look for it's
public struct String {
and AnyObject refers to any instance of a class only , and is equivalent to id in Objective-C , so this declaration of x
[String: String] doesn't fit with [String: AnyObject]
because Any refers to any instance of a class, struct, or enum so it'll fit perfectly