How can I make a Decodable object from a dictionary?

Viewed 7766

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
...
2 Answers

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:

  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.
Related