Init an Object Conforming to Codable with a Dictionary/Array

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

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

Implement Codable for an Array of Dictionary with failable responses

This is a solution widely based on this intriguing answer of Rob Napier.

The goal of TitleKey and the two Decoder extensions is to map dictionaries with arbitrary keys to arrays adding the key as title property.

struct TitleKey: CodingKey {
let stringValue: String
init?(stringValue: String) { self.stringValue = stringValue }
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
}

extension Decoder {
func currentTitle() throws -> String {
guard let titleKey = codingPath.last as? TitleKey else {
throw DecodingError.dataCorrupted(.init(codingPath: codingPath,
debugDescription: "Not in titled container"))
}
return titleKey.stringValue
}
}

extension Decoder {
func decodeTitledElements<Element: Decodable>(_ type: Element.Type) throws -> [Element] {
let titles = try container(keyedBy: TitleKey.self)
return titles.allKeys.compactMap { title in
return try? titles.decode(Element.self, forKey: title)
}
}
}

I modified the decodeTitledElements function to decode only those dictionaries whose value represents the RecordSuper struct filtering the other keys.

Here are the structs.

struct Root : Decodable {
let data : [Containers]
}

struct Containers: Decodable {
let containers: [RecordSuper]
init(from decoder: Decoder) throws {
self.containers = try decoder.decodeTitledElements(RecordSuper.self)
}
}

struct RecordSuper : Decodable {
let title : String
let dateTime : Date
let medication : String
let record : Record

enum CodingKeys: String, CodingKey {
case dateTime = "date_time", medication, record
}

init(from decoder: Decoder) throws {
self.title = try decoder.currentTitle()
let container = try decoder.container(keyedBy: CodingKeys.self)
self.dateTime = try container.decode(Date.self, forKey: .dateTime)
self.medication = try container.decode(String.self, forKey: .medication)
self.record = try container.decode(Record.self, forKey: .record)
}
}

struct Record : Decodable {
let status : String
}

Now decode the JSON assuming jsonData is the JSON as Data

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .millisecondsSince1970
let result = try decoder.decode(Root.self, from: jsonData
print(result.data)

How can I make a Decodable object from a dictionary?

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:

  1. create a JSONdecoder object.
  2. use that JSONdecoder to decode an object of type ResponseModel
  3. assign the result of the decoding to self. This way all properties of self are assigned.

Am I able to add a member-wise initializer to a Codable-conforming object?

Your code is correct, double check whether you actually have a public initializer not a private one.

I put this into my Xcode Playground and everything works as expected:

public class A: Codable {
var aString: String?
public init(input: String?) {
self.aString = input
}
}

let a = A(input: "123")

How to decode a property with type of JSON dictionary in Swift [45] decodable protocol

With some inspiration from this gist I found, I wrote some extensions for UnkeyedDecodingContainer and KeyedDecodingContainer. You can find a link to my gist here. By using this code you can now decode any Array<Any> or Dictionary<String, Any> with the familiar syntax:

let dictionary: [String: Any] = try container.decode([String: Any].self, forKey: key)

or

let array: [Any] = try container.decode([Any].self, forKey: key)

Edit: there is one caveat I have found which is decoding an array of dictionaries [[String: Any]] The required syntax is as follows. You'll likely want to throw an error instead of force casting:

let items: [[String: Any]] = try container.decode(Array<Any>.self, forKey: .items) as! [[String: Any]]

EDIT 2: If you simply want to convert an entire file to a dictionary, you are better off sticking with api from JSONSerialization as I have not figured out a way to extend JSONDecoder itself to directly decode a dictionary.

guard let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
// appropriate error handling
return
}

The extensions

// Inspired by https://gist.github.com/mbuchetics/c9bc6c22033014aa0c550d3b4324411a

struct JSONCodingKeys: CodingKey {
var stringValue: String

init?(stringValue: String) {
self.stringValue = stringValue
}

var intValue: Int?

init?(intValue: Int) {
self.init(stringValue: "\(intValue)")
self.intValue = intValue
}
}


extension KeyedDecodingContainer {

func decode(_ type: Dictionary<String, Any>.Type, forKey key: K) throws -> Dictionary<String, Any> {
let container = try self.nestedContainer(keyedBy: JSONCodingKeys.self, forKey: key)
return try container.decode(type)
}

func decodeIfPresent(_ type: Dictionary<String, Any>.Type, forKey key: K) throws -> Dictionary<String, Any>? {
guard contains(key) else {
return nil
}
guard try decodeNil(forKey: key) == false else {
return nil
}
return try decode(type, forKey: key)
}

func decode(_ type: Array<Any>.Type, forKey key: K) throws -> Array<Any> {
var container = try self.nestedUnkeyedContainer(forKey: key)
return try container.decode(type)
}

func decodeIfPresent(_ type: Array<Any>.Type, forKey key: K) throws -> Array<Any>? {
guard contains(key) else {
return nil
}
guard try decodeNil(forKey: key) == false else {
return nil
}
return try decode(type, forKey: key)
}

func decode(_ type: Dictionary<String, Any>.Type) throws -> Dictionary<String, Any> {
var dictionary = Dictionary<String, Any>()

for key in allKeys {
if let boolValue = try? decode(Bool.self, forKey: key) {
dictionary[key.stringValue] = boolValue
} else if let stringValue = try? decode(String.self, forKey: key) {
dictionary[key.stringValue] = stringValue
} else if let intValue = try? decode(Int.self, forKey: key) {
dictionary[key.stringValue] = intValue
} else if let doubleValue = try? decode(Double.self, forKey: key) {
dictionary[key.stringValue] = doubleValue
} else if let nestedDictionary = try? decode(Dictionary<String, Any>.self, forKey: key) {
dictionary[key.stringValue] = nestedDictionary
} else if let nestedArray = try? decode(Array<Any>.self, forKey: key) {
dictionary[key.stringValue] = nestedArray
}
}
return dictionary
}
}

extension UnkeyedDecodingContainer {

mutating func decode(_ type: Array<Any>.Type) throws -> Array<Any> {
var array: [Any] = []
while isAtEnd == false {
// See if the current value in the JSON array is `null` first and prevent infite recursion with nested arrays.
if try decodeNil() {
continue
} else if let value = try? decode(Bool.self) {
array.append(value)
} else if let value = try? decode(Double.self) {
array.append(value)
} else if let value = try? decode(String.self) {
array.append(value)
} else if let nestedDictionary = try? decode(Dictionary<String, Any>.self) {
array.append(nestedDictionary)
} else if let nestedArray = try? decode(Array<Any>.self) {
array.append(nestedArray)
}
}
return array
}

mutating func decode(_ type: Dictionary<String, Any>.Type) throws -> Dictionary<String, Any> {

let nestedContainer = try self.nestedContainer(keyedBy: JSONCodingKeys.self)
return try nestedContainer.decode(type)
}
}

Codable [String: Any] dictionary


Is it possible to use Codable with [String: Any] dictionary?

no, you can't with Codable but you can make this dictionary in a new model.

Replace

public var message: [String: Any]?

with

public var message: MessageModel?

struct MessageModel: Codable { }

How to encode/decode a dictionary with Codable values for storage in UserDefaults?

You are mixing up NSCoding and Codable. The former requires a subclass of NSObject, the latter can encode the structs and classes directly with JSONEncoder or ProperListEncoder without any Keyedarchiver which also belongs to NSCoding.

Your struct can be reduced to

struct Company: Codable {
var name : String
var initials : String
var logoURL : URL?
var brandColor : String?
}

That's all, the CodingKeys and the other methods are synthesized. I would at least declare name and initials as non-optional.

To read and save the data is pretty straightforward. The corresponding CompanyDefaults struct is

struct CompanyDefaults {

static private let companiesKey = "companiesKey"

static var companies: [String:Company] = {

guard let data = UserDefaults.standard.data(forKey: companiesKey) else { return [:] }
return try? JSONDecoder.decode([String:Company].self, from: data) ?? [:]
}() {
didSet {
guard let data = try? JSONEncoder().encode(companies) else { return }
UserDefaults.standard.set(data, forKey: companiesKey)
}
}
}


Related Topics



Leave a reply



Submit