How to create an instance of a Decoder? - swift

There is struct from another module that I would like to initialize. It doesn't have a public init() method but I can create it from decoding JSON.
struct Foo: Codable {
let name: String
let details: String
}
I have created a static function to create an instance
extension Foo {
static func create(name: String) throws -> Self {
let dictionaryRep = ["name": name, "details": ""]
let data = try JSONSerialization.data(withJSONObject: dictionaryRep, options: [])
return try JSONDecoder().decode(Self.self, from: data)
}
}
Is there instead a way to create a new init method to create a new instance?
extension {
public init(name: String) throws {
try self.init(from: <#Decoder#>)
}
}

Because Foo is a struct (not a class), your initializer can assign to self like this:
extension Foo {
init(name: String) throws {
let data = try JSONSerialization.data(withJSONObject: ["name": name, "details": ""])
self = try JSONDecoder().decode(Self.self, from: data)
}
}

Related

Swift: Insert codable object into Core Data

I'm getting a response from an API and decoding the response like this:
struct MyStuff: Codable {
let name: String
let quantity: Int
let location: String
}
And I have instance an Entity to map MyStuff:
#objc(Stuff)
public class Stuff: NSManagedObject {
}
extension Stuff {
#nonobjc public class func fetchRequest() -> NSFetchRequest<Stuff> {
return NSFetchRequest<Stuff>(entityName: "Stuff")
}
#NSManaged public var name: String?
#NSManaged public var quantity: Int64
#NSManaged public var location: String?
}
My question is, when I have the response of type MyStuff there is a way to loop thru the keys and map the values to core data?
for example:
let myStuff = MyStuff(name: "table", quantity: 1, location: "kitchen")
let myStuff = MyStuff(name: "table", quantity: 1, location: "kitchen")
for chidren in Mirror(reflecting: myStuff).children {
print(chidren.label)
print(chidren.value)
/*
insert values to core data
*/
}
I'll really appreciate your help
A smart solution is to adopt Decodable in Stuff
Write an extension of CodingUserInfoKey and JSONDecoder
extension CodingUserInfoKey {
static let context = CodingUserInfoKey(rawValue: "context")!
}
extension JSONDecoder {
convenience init(context: NSManagedObjectContext) {
self.init()
self.userInfo[.context] = context
}
}
In Stuff adopt Decodable and implement init(from:), it must be implemented in the class, not in the extension
#objc(Stuff)
public class Stuff: NSManagedObject, Decodable {
private enum CodingKeys: String, CodingKey { case name, quantity, location }
public required convenience init(from decoder: Decoder) throws {
guard let context = decoder.userInfo[.context] as? NSManagedObjectContext else { fatalError("Error: context doesn't exist!") }
let entity = NSEntityDescription.entity(forEntityName: "Stuff", in: context)!
self.init(entity: entity, insertInto: context)
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decodeIfPresent(String.self, forKey: .name)
quantity = try values.decodeIfPresent(Int64.self, forKey: .quantity) ?? 0
location = try values.decodeIfPresent(String.self, forKey: .location)
}
}
To decode the JSON you have to initialize the decoder with the convenience initializer
let decoder = JSONDecoder(context: context)
where context is the current NSManagedObjectContext instance.
Now you can create Stuff instances directly from the JSON.
You can store entire object as JSONString if you don't support query for each field.
If you need query for some field then keep that field in entity object.
struct MyStuff: Codable {
let name: String
let quantity: Int
let location: String
}
extension Encodable {
func toString() -> String? {
if let config = try? JSONEncoder().encode(self) {
return String(data: config, encoding: .utf8)
}
return .none
}
}
extension Decodable {
static func map(JSONString: String) -> Self? {
try? JSONDecoder().decode(Self.self, from: JSONString.data(using: .utf8) ?? .init())
}
}
#objc(Stuff)
public class Stuff: NSManagedObject {
}
// Entity with single field (no field base query support)
extension Stuff {
#nonobjc public class func fetchRequest() -> NSFetchRequest<Stuff> {
return NSFetchRequest<Stuff>(entityName: "Stuff")
}
#NSManaged public var myStuffRawJSON: String?
func mapToMyStuff() -> MyStuff? {
MyStuff.map(JSONString: myStuffRawJSON ?? "")
}
}
How to use:
let myStuff = MyStuff(name: "table", quantity: 1, location: "kitchen")
let entity: Stuff //Create entity
entity.myStuffRawJSON = myStuff.toString()
// save your entity

how do you set nil to codable enum when unexpected value in swift iOS

I'm using Codable for my WebRequest response which is returning some predefined string or number. So, I'm using Enums for those. But when some unexpected value arrive to Response at that my Codable fails to decode.
Here some code for better understanding.
class WebUser: Codable, Equatable {
static func == (lhs: WebUser, rhs: WebUser) -> Bool {
return lhs.id == rhs.id
}
...
var mobileNumberPrivacy: CommonPrivacyOption?
var emailPrivacy: CommonPrivacyOption?
var dobPrivacy: CommonPrivacyOption?
...
}
enum CommonPrivacyOption: Int, CaseIterable, Codable {
case privacyOnlyMe = 1, privacyPublic, privacyFriends, privacyFriendsOfFriends
//Does not help this optional init function
/*init?(rawValue: Int) {
switch rawValue {
case 1: self = .privacyOnlyMe
case 2: self = .privacyPublic
case 3: self = .privacyFriends
case 4: self = .privacyFriendsOfFriends
default: return nil
}
}*/
}
but sometimes from WebServer I'm getting, 0 for dobPrivacy at that time I'm getting DecodingError.dataCorrupted exception with context Cannot initialize CommonPrivacyOption from invalid Int value 0
As I expect to dobPrivacy nil when I get other values then 1/2/3/4.
EDIT:
let dict1 = [
"id": 2,
"mobileNumberPrivacy": 3,
"emailPrivacy": 4,
"dobPrivacy": 0 // Works perfectly with 1
]
do {
let data1 = try JSONSerialization.data(withJSONObject: dict1, options: .prettyPrinted)
let user1 = try JSONDecoder().decode(WebUser.self, from: data1)
print("User1 created")
}
catch DecodingError.dataCorrupted(let context) {
print(context.codingPath)
print(context.debugDescription)
}
catch {
print(error.localizedDescription)
}
I'm using this same Codable WebUser object for Profile detail, search users and many more.
so may be some times one more key will not present in WebRequest's response.
I recommend writing a property wrapper that handles this problem for you.
Specifically, let's write a property wrapper named NilOnDecodingError that turns any DecodingError into nil.
Here's the declaration of NilOnDecodingError:
#propertyWrapper
public struct NilOnDecodingError<Wrapped> {
public init(wrappedValue: Wrapped?) {
self.wrappedValue = wrappedValue
}
public var wrappedValue: Wrapped?
}
We've defined it to wrap any type, storing an Optional.
Now we can conform it to Decodable when the Wrapped type is Decodable:
extension NilOnDecodingError: Decodable where Wrapped: Decodable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
wrappedValue = .some(try container.decode(Wrapped.self))
} catch is DecodingError {
wrappedValue = nil
}
}
}
We probably also want it to be Encodable when the Wrapped type is Encodable:
extension NilOnDecodingError: Encodable where Wrapped: Encodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
if let value = wrappedValue {
try container.encode(value)
} else {
try container.encodeNil()
}
}
}
Now we can wrap the appropriate fields of WebUser:
class WebUser: Codable {
let id: String
#NilOnDecodingError
var mobileNumberPrivacy: CommonPrivacyOption?
#NilOnDecodingError
var emailPrivacy: CommonPrivacyOption?
#NilOnDecodingError
var dobPrivacy: CommonPrivacyOption?
}
For testing, we'll want to print the fields of the decoded user:
extension WebUser: CustomStringConvertible {
var description: String {
return """
WebUser(
id: \(id),
mobileNumberPrivacy: \(mobileNumberPrivacy.map { "\($0)" } ?? "nil"),
emailPrivacy: \(emailPrivacy.map { "\($0)" } ?? "nil")),
dobPrivacy: \(dobPrivacy.map { "\($0)" } ?? "nil")))
"""
}
}
Now we can try it out:
let json = """
{
"id": "mrugesh",
"mobileNumberPrivacy": 1,
"emailPrivacy": 2,
"dobPrivacy": 1000
}
"""
let user = try! JSONDecoder().decode(WebUser.self, from: json.data(using: .utf8)!)
print(user)
Output:
WebUser(
id: mrugesh,
mobileNumberPrivacy: privacyOnlyMe,
emailPrivacy: privacyPublic),
dobPrivacy: nil))
You need to create a custom Decodable initializer inside WebUser:
class WebUser: Codable {
var dobPrivacy: CommonPrivacyOption?
// Rest of your properties here.
enum CodingKeys: String, CodingKey {
case dobPrivacy
// Add a case for each property you want to decode here.
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
// Use optional try to decode your enum so that when the
// decode fails because of wrong Int value, it will assign nil.
dobPrivacy = try? container.decode(CommonPrivacyOption.self, forKey: .dobPrivacy)
}
}
Alternatively, you can implement the Decodable initializer inside CommonPrivacyOption and add an additional case unknown like so:
enum CommonPrivacyOption: Int, Codable {
case privacyOnlyMe = 1
case privacyPublic, privacyFriends, privacyFriendsOfFriends
case unknown
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let value = try container.decode(Int.self)
// Try to initialize Self from value, if
// value is not 1, 2, 3, or 4, initialize Self to
// the unknown case.
self = .init(rawValue: value) ?? .unknown
}
}
It looks to me like the compiler selects the wrong init for the enum types, instead of init(rawValue) it uses init(from:) that is the one for decoding (which in a way makes sense)
Here is a solution where we override this behaviour by using a custom init(from) in WebUser that decodes the raw values and then creates an enum item
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let value = try container.decodeIfPresent(CommonPrivacyOption.RawValue.self, forKey: .mobileNumberPrivacy), let mobileNumberPrivacy = CommonPrivacyOption(rawValue: value) {
self.mobileNumberPrivacy = mobileNumberPrivacy
}
if let value = try container.decodeIfPresent(CommonPrivacyOption.RawValue.self, forKey: .emailPrivacy), let emailPrivacy = CommonPrivacyOption(rawValue: value) {
self.emailPrivacy = emailPrivacy
}
if let value = try container.decodeIfPresent(CommonPrivacyOption.RawValue.self, forKey: .dobPrivacy), let dobPrivacy = CommonPrivacyOption(rawValue: value) {
self.dobPrivacy = dobPrivacy
}
}
Below is a small example
extension WebUser: CustomStringConvertible {
var description: String {
"Mobile: \(mobileNumberPrivacy?.rawValue), email: \(emailPrivacy?.rawValue), dob: \(dobPrivacy?.rawValue)"
}
}
let data = """
{
"mobileNumberPrivacy": 1,
"dobPrivacy": 0
}
""".data(using: .utf8)!
do {
let result = try JSONDecoder().decode(WebUser.self, from: data)
print(result)
} catch {
print(error)
}
Mobile: Optional(1), email: nil, dob: nil
Of course if you can change your mind about 0 being translated to nil then I would suggest you extend the enum to support the 0 value
enum CommonPrivacyOption: Int, CaseIterable, Codable {
case none = 0
case privacyOnlyMe = 1, privacyPublic, privacyFriends, privacyFriendsOfFriends
}
Then it should work out of the box and you don't need to write any custom code.
I liked #alobaili's answer since it's simple and provided a good solution. One thing I wanted to improve is to make it more generic, so that any Decodable (and Codable) could do it with less code to write.
extension Decodable where Self: RawRepresentable, RawValue: Decodable {
static func initializedOptionalWith(decoder: Decoder, defaultValue: Self) throws -> Self {
let container = try decoder.singleValueContainer()
let value = try container.decode(RawValue.self)
return .init(rawValue: value) ?? defaultValue
}
}
Usage:
init(from decoder: Decoder) throws {
self = try .initializedOptionalWith(decoder: decoder, defaultValue: .unknown)
}
Thank you Rob Mayoff for a great answer. I would like to add just one thing - if the property is missing, decoding fails even if the property is optional. To prevent this failure, add the following extension:
extension KeyedDecodingContainer {
func decode<T>(_ type: NilOnDecodingError<T>.Type, forKey key: Self.Key) throws -> NilOnDecodingError<T> where T: Decodable {
try decodeIfPresent(type, forKey: key) ?? NilOnDecodingError<T>(wrappedValue: nil)
}
}

Create a generic Data initializer for Decodable types using Swift

#objcMembers
public class MyResponse: NSObject, Codable {
public let id: String?
public let context: String?
public let results: [MyResult]?
}
What is the proper way to parse MyResponse from Data in class extension?
I tried the following, but got error "Cannot assign to value: 'self' is immutable.
Cannot assign value of type 'MyResponse' to type 'Self'."
extension MyResponse {
public convenience init(data: Data) throws {
self = try JSONDecoder().decode(MyResponse.self, from: data)
}
}
You can extend Decodable protocol and create a generic initializer:
extension Decodable {
public init(data: Data, using decoder: JSONDecoder = JSONDecoder()) throws {
self = try decoder.decode(Self.self, from: data)
}
}
You can't overwrite class itself, but you can init it, init object from json and then assign values/ If take your code - it'll be something like this:
public class MyResponse: Codable {
public var id: String?
public var context: String?
public var results: [MyResult]?
}
public extension MyResponse {
convenience init(data: Data) throws {
self.init()
let object = try JSONDecoder().decode(MyResponse.self, from: data)
self.id = object.id
self.context = object.context
self.results = object.results
}
}
If you really don't need a class it's better to use struct instead of it, and it can be like this:
public struct MyResponse: Codable {
public let id: String?
public let context: String?
public let results: [String]?
}
public extension MyResponse {
init(data: Data) throws {
self = try JSONDecoder().decode(MyResponse.self, from: data)
}
}

How to encode Realm's List<> type

I am trying to encode my Realm database to JSON. Everything is working except the List<> encoding. So my question is, how would you encode List<>? Because the List doesn't conform to Encodable neighter Decodable protocol.
Right now I am doing this:
#objcMembers class User: Object, Codable{
dynamic var name: String = ""
let dogs = List<Dog>()
private enum UserCodingKeys: String, CodingKey {
case name
case dogs
}
convenience init(name: String) {
self.init()
self.name = name
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: UserCodingKeys.self)
try container.encode(name, forKey: .name)
}
#objcMembers class Dog: Object, Codable{
dynamic var name: String = ""
dynamic var user: User? = nil
private enum DogCodingKeys: String, CodingKey {
case name
}
convenience init(name: String) {
self.init()
name = name
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: DogCodingKeys.self)
try container.encode(name, forKey: .name)
}
}
and like this I am trying to do it:
var json: Any?
let user = RealmService.shared.getUsers()
var usersArray = [User]()
for user in users{
usersArray.append(user)
}
let jsonEncoder = JSONEncoder()
let jsonDecoder = JSONDecoder()
let encodedJson = try? jsonEncoder.encode(portfoliosArray)
if let data = encodedJson {
json = try? JSONSerialization.jsonObject(with: data, options: .allowFragments)
if let json = json {
print(String(describing: json))
}
}
So the question is how I am able to encode the List<Dog>?
To make a Realm object model class with a property of type List conform to Encodable, you can simply convert the List to an Array in the encode(to:) method, which can be encoded automatically.
extension User: Encodable {
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.username, forKey: .username)
let dogsArray = Array(self.dogs)
try container.encode(dogsArray, forKey: .dogs)
}
}
Test classes I used (slightly different from the ones in your question, but I already had these on hand and the methods in question will be almost identical regardless of the variable names):
class Dog: Object,Codable {
#objc dynamic var id:Int = 0
#objc dynamic var name:String = ""
}
class User: Object, Decodable {
#objc dynamic var id:Int = 0
#objc dynamic var username:String = ""
#objc dynamic var email:String = ""
let dogs = List<Dog>()
private enum CodingKeys: String, CodingKey {
case id, username, email, dogs
}
required convenience init(from decoder: Decoder) throws {
self.init()
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
username = try container.decode(String.self, forKey: .username)
email = try container.decode(String.self, forKey: .email)
let dogsArray = try container.decode([Dog].self, forKey: .dogs)
dogs.append(objectsIn: dogsArray)
}
}
Test the encoding/decoding:
let userJSON = """
{
"id":1,
"username":"John",
"email":"example#ex.com",
"dogs":[
{"id":2,"name":"King"},
{"id":3,"name":"Kong"}
]
}
"""
do {
let decodedUser = try JSONDecoder().decode(User.self, from: userJSON.data(using: .utf8)!)
let encodedUser = try JSONEncoder().encode(decodedUser)
print(String(data: encodedUser, encoding: .utf8)!)
} catch {
print(error)
}
Output:
{"username":"John","dogs":[{"id":2,"name":"King"},{"id":3,"name":"Kong"}]}
You could resort to a mini-hack by making List conform to Encodable:
extension List: Encodable {
public func encode(to coder: Encoder) throws {
// by default List is not encodable, throw an exception
throw NSError(domain: "SomeDomain", code: -1, userInfo: nil)
}
}
// let's ask it to nicely encode when Element is Encodable
extension List where Element: Encodable {
public func encode(to coder: Encoder) throws {
var container = coder.unkeyedContainer()
try container.encode(contentsOf: self)
}
}
Two extensions are needed as you can't add protocol conformance and where clauses at the same time.
Also note that this approach doesn't provide compile-time checks - e.g. a List<Cat> will throw an exception an runtime if Cat is not encodable, instead of a nice compile time error.
The upside is lot of boilerplate code no longer needed:
#objcMembers class User: Object, Encodable {
dynamic var name: String = ""
let dogs = List<Dog>()
convenience init(name: String) {
self.init()
self.name = name
}
}
#objcMembers class Dog: Object, Encodable {
dynamic var name: String = ""
dynamic var user: User? = nil
convenience init(name: String) {
self.init()
name = name
}
}
This is also scalable, as adding new classes don't require any encoding code, but with the mentioned downside of not being fully type safe at compile time.

How can I use Swift’s Codable to encode into a dictionary?

I have a struct that implements Swift 4’s Codable. Is there a simple built-in way to encode that struct into a dictionary?
let struct = Foo(a: 1, b: 2)
let dict = something(struct)
// now dict is ["a": 1, "b": 2]
If you don't mind a bit of shifting of data around you could use something like this:
extension Encodable {
func asDictionary() throws -> [String: Any] {
let data = try JSONEncoder().encode(self)
guard let dictionary = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else {
throw NSError()
}
return dictionary
}
}
Or an optional variant
extension Encodable {
var dictionary: [String: Any]? {
guard let data = try? JSONEncoder().encode(self) else { return nil }
return (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)).flatMap { $0 as? [String: Any] }
}
}
Assuming Foo conforms to Codable or really Encodable then you can do this.
let struct = Foo(a: 1, b: 2)
let dict = try struct.asDictionary()
let optionalDict = struct.dictionary
If you want to go the other way(init(any)), take a look at this Init an object conforming to Codable with a dictionary/array
Here are simple implementations of DictionaryEncoder / DictionaryDecoder that wrap JSONEncoder, JSONDecoder and JSONSerialization, that also handle encoding / decoding strategies…
class DictionaryEncoder {
private let encoder = JSONEncoder()
var dateEncodingStrategy: JSONEncoder.DateEncodingStrategy {
set { encoder.dateEncodingStrategy = newValue }
get { return encoder.dateEncodingStrategy }
}
var dataEncodingStrategy: JSONEncoder.DataEncodingStrategy {
set { encoder.dataEncodingStrategy = newValue }
get { return encoder.dataEncodingStrategy }
}
var nonConformingFloatEncodingStrategy: JSONEncoder.NonConformingFloatEncodingStrategy {
set { encoder.nonConformingFloatEncodingStrategy = newValue }
get { return encoder.nonConformingFloatEncodingStrategy }
}
var keyEncodingStrategy: JSONEncoder.KeyEncodingStrategy {
set { encoder.keyEncodingStrategy = newValue }
get { return encoder.keyEncodingStrategy }
}
func encode<T>(_ value: T) throws -> [String: Any] where T : Encodable {
let data = try encoder.encode(value)
return try JSONSerialization.jsonObject(with: data, options: .allowFragments) as! [String: Any]
}
}
class DictionaryDecoder {
private let decoder = JSONDecoder()
var dateDecodingStrategy: JSONDecoder.DateDecodingStrategy {
set { decoder.dateDecodingStrategy = newValue }
get { return decoder.dateDecodingStrategy }
}
var dataDecodingStrategy: JSONDecoder.DataDecodingStrategy {
set { decoder.dataDecodingStrategy = newValue }
get { return decoder.dataDecodingStrategy }
}
var nonConformingFloatDecodingStrategy: JSONDecoder.NonConformingFloatDecodingStrategy {
set { decoder.nonConformingFloatDecodingStrategy = newValue }
get { return decoder.nonConformingFloatDecodingStrategy }
}
var keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy {
set { decoder.keyDecodingStrategy = newValue }
get { return decoder.keyDecodingStrategy }
}
func decode<T>(_ type: T.Type, from dictionary: [String: Any]) throws -> T where T : Decodable {
let data = try JSONSerialization.data(withJSONObject: dictionary, options: [])
return try decoder.decode(type, from: data)
}
}
Usage is similar to JSONEncoder / JSONDecoder…
let dictionary = try DictionaryEncoder().encode(object)
and
let object = try DictionaryDecoder().decode(Object.self, from: dictionary)
For convenience, I've put this all in a repo… https://github.com/ashleymills/SwiftDictionaryCoding
I have create a library called CodableFirebase and it's initial purpose was to use it with Firebase Database, but it does actually what you need: it creates a dictionary or any other type just like in JSONDecoder but you don't need to do the double conversion here like you do in other answers. So it would look something like:
import CodableFirebase
let model = Foo(a: 1, b: 2)
let dict = try! FirebaseEncoder().encode(model)
There is no built in way to do that.
As answered above if you have no performance issues then you can accept the JSONEncoder + JSONSerialization implementation.
But I would rather go the standard library's way to provide an encoder/decoder object.
class DictionaryEncoder {
private let jsonEncoder = JSONEncoder()
/// Encodes given Encodable value into an array or dictionary
func encode<T>(_ value: T) throws -> Any where T: Encodable {
let jsonData = try jsonEncoder.encode(value)
return try JSONSerialization.jsonObject(with: jsonData, options: .allowFragments)
}
}
class DictionaryDecoder {
private let jsonDecoder = JSONDecoder()
/// Decodes given Decodable type from given array or dictionary
func decode<T>(_ type: T.Type, from json: Any) throws -> T where T: Decodable {
let jsonData = try JSONSerialization.data(withJSONObject: json, options: [])
return try jsonDecoder.decode(type, from: jsonData)
}
}
You can try it with following code:
struct Computer: Codable {
var owner: String?
var cpuCores: Int
var ram: Double
}
let computer = Computer(owner: "5keeve", cpuCores: 8, ram: 4)
let dictionary = try! DictionaryEncoder().encode(computer)
let decodedComputer = try! DictionaryDecoder().decode(Computer.self, from: dictionary)
I am force-trying here to make the example shorter. In production code you should handle the errors appropriately.
I'm not sure if it's the best way but you definitely can do something like:
struct Foo: Codable {
var a: Int
var b: Int
init(a: Int, b: Int) {
self.a = a
self.b = b
}
}
let foo = Foo(a: 1, b: 2)
let dict = try JSONDecoder().decode([String: Int].self, from: JSONEncoder().encode(foo))
print(dict)
let dict = try JSONSerialization.jsonObject(with: try JSONEncoder().encode(struct), options: []) as? [String: Any]
I have modified the PropertyListEncoder from the Swift project into a DictionaryEncoder, simply by removing the final serialisation from dictionary into binary format. You can do the same yourself, or you can take my code from here
It can be used like this:
do {
let employeeDictionary: [String: Any] = try DictionaryEncoder().encode(employee)
} catch let error {
// handle error
}
In some project, i'm used the swift reflection. But be careful, nested codable objects, are not mapped also there.
let dict = Dictionary(uniqueKeysWithValues: Mirror(reflecting: foo).children.map{ ($0.label!, $0.value) })
I definitely think that there's some value in just being able to use Codable to encode to/from dictionaries, without the intention of ever hitting JSON/Plists/whatever. There are plenty of APIs which just give you back a dictionary, or expect a dictionary, and it's nice to be able to interchange them easily with Swift structs or objects, without having to write endless boilerplate code.
I've been playing round with some code based on the Foundation JSONEncoder.swift source (which actually does implement dictionary encoding/decoding internally, but doesn't export it).
The code can be found here: https://github.com/elegantchaos/DictionaryCoding
It's still quite rough, but I've expanded it a bit so that, for example, it can fill in missing values with defaults when decoding.
Here is a protocol based solution:
protocol DictionaryEncodable {
func encode() throws -> Any
}
extension DictionaryEncodable where Self: Encodable {
func encode() throws -> Any {
let jsonData = try JSONEncoder().encode(self)
return try JSONSerialization.jsonObject(with: jsonData, options: .allowFragments)
}
}
protocol DictionaryDecodable {
static func decode(_ dictionary: Any) throws -> Self
}
extension DictionaryDecodable where Self: Decodable {
static func decode(_ dictionary: Any) throws -> Self {
let jsonData = try JSONSerialization.data(withJSONObject: dictionary, options: [])
return try JSONDecoder().decode(Self.self, from: jsonData)
}
}
typealias DictionaryCodable = DictionaryEncodable & DictionaryDecodable
And here is how to use it:
class AClass: Codable, DictionaryCodable {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
struct AStruct: Codable, DictionaryEncodable, DictionaryDecodable {
var name: String
var age: Int
}
let aClass = AClass(name: "Max", age: 24)
if let dict = try? aClass.encode(), let theClass = try? AClass.decode(dict) {
print("Encoded dictionary: \n\(dict)\n\ndata from decoded dictionary: \"name: \(theClass.name), age: \(theClass.age)\"")
}
let aStruct = AStruct(name: "George", age: 30)
if let dict = try? aStruct.encode(), let theStruct = try? AStruct.decode(dict) {
print("Encoded dictionary: \n\(dict)\n\ndata from decoded dictionary: \"name: \(theStruct.name), age: \(theStruct.age)\"")
}
I wrote a quick gist to handle this (not using the Codable protocol). Be careful, it doesn't type-check any values and doesn't work recursively on values that are encodable.
class DictionaryEncoder {
var result: [String: Any]
init() {
result = [:]
}
func encode(_ encodable: DictionaryEncodable) -> [String: Any] {
encodable.encode(self)
return result
}
func encode<T, K>(_ value: T, key: K) where K: RawRepresentable, K.RawValue == String {
result[key.rawValue] = value
}
}
protocol DictionaryEncodable {
func encode(_ encoder: DictionaryEncoder)
}
There no straight forward way of doing this in Codable. You need to implement Encodable/Decodable protocol for your struct. For your example, you might need to write as below
typealias EventDict = [String:Int]
struct Favorite {
var all:EventDict
init(all: EventDict = [:]) {
self.all = all
}
}
extension Favorite: Encodable {
struct FavoriteKey: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: FavoriteKey.self)
for eventId in all {
let nameKey = FavoriteKey(stringValue: eventId.key)!
try container.encode(eventId.value, forKey: nameKey)
}
}
}
extension Favorite: Decodable {
public init(from decoder: Decoder) throws {
var events = EventDict()
let container = try decoder.container(keyedBy: FavoriteKey.self)
for key in container.allKeys {
let fav = try container.decode(Int.self, forKey: key)
events[key.stringValue] = fav
}
self.init(all: events)
}
}
I have made a pod here https://github.com/levantAJ/AnyCodable to facilitate decode and encode [String: Any] and [Any]
pod 'DynamicCodable', '1.0'
And you are able to decode & encode [String: Any] and [Any]
import DynamicCodable
struct YourObject: Codable {
var dict: [String: Any]
var array: [Any]
var optionalDict: [String: Any]?
var optionalArray: [Any]?
enum CodingKeys: String, CodingKey {
case dict
case array
case optionalDict
case optionalArray
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
dict = try values.decode([String: Any].self, forKey: .dict)
array = try values.decode([Any].self, forKey: .array)
optionalDict = try values.decodeIfPresent([String: Any].self, forKey: .optionalDict)
optionalArray = try values.decodeIfPresent([Any].self, forKey: .optionalArray)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(dict, forKey: .dict)
try container.encode(array, forKey: .array)
try container.encodeIfPresent(optionalDict, forKey: .optionalDict)
try container.encodeIfPresent(optionalArray, forKey: .optionalArray)
}
}
After research, we find that if we use the keyword Any in the class which is inherited from the Codable & Decodable it will give the error. So if you want to use a dictionary user with the types of data coming from the server.
For example, the server is sending the dictionary of type [String : Int] then use [String : Int] if you will try [String : Any] it will not work.
Here is dictionary -> object. Swift 5.
extension Dictionary where Key == String, Value: Any {
func object<T: Decodable>() -> T? {
if let data = try? JSONSerialization.data(withJSONObject: self, options: []) {
return try? JSONDecoder().decode(T.self, from: data)
} else {
return nil
}
}
}
Come to think of it, the question does not have an answer in the general case, since the Encodable instance may be something not serializable into a dictionary, such as an array:
let payload = [1, 2, 3]
let encoded = try JSONEncoder().encode(payload) // "[1,2,3]"
Other than that, I have written something similar as a framework.