Init an object conforming to Codable with a dictionary/array

14,241

Solution 1

At the moment the best solution I have is this but it has the overhead of encoding/decoding.

extension Decodable {
  init(from: Any) throws {
    let data = try JSONSerialization.data(withJSONObject: from, options: .prettyPrinted)
    let decoder = JSONDecoder()
    self = try decoder.decode(Self.self, from: data)
  }
}

Following from the example in the question the result would be

let person = Person(from: dictionary)

If you're interested in going the other way then this might help https://stackoverflow.com/a/46329055/1453346

Solution 2

based on Chris Mitchelmore answer

Details

  • Xcode Version 10.3 (10G8), Swift 5

Solution

import Foundation

extension Decodable {

    init(from value: Any,
         options: JSONSerialization.WritingOptions = [],
         decoder: JSONDecoder) throws {
        let data = try JSONSerialization.data(withJSONObject: value, options: options)
        self = try decoder.decode(Self.self, from: data)
    }

    init(from value: Any,
         options: JSONSerialization.WritingOptions = [],
         decoderSetupClosure: ((JSONDecoder) -> Void)? = nil) throws {
        let decoder = JSONDecoder()
        decoderSetupClosure?(decoder)
        try self.init(from: value, options: options, decoder: decoder)
    }

    init?(discardingAnErrorFrom value: Any,
          printError: Bool = false,
          options: JSONSerialization.WritingOptions = [],
          decoderSetupClosure: ((JSONDecoder) -> Void)? = nil) {
        do {
            try self.init(from: value, options: options, decoderSetupClosure: decoderSetupClosure)
        } catch {
            if printError { print("\(Self.self) decoding ERROR:\n\(error)") }
            return nil
        }
    }
}

Usage

struct Item: Decodable {
    let id: Int
    let name: String
    let isActive: Bool
    var date: Date
}

let dictionary = ["id": 1, "name": "Item", "is_active": false,
                  "date": "2019-08-06T06:55:00.000-04:00"] as [String : Any]
do {
    let item1 = try Item(from: dictionary) { decoder in
        decoder.keyDecodingStrategy = .convertFromSnakeCase
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
        decoder.dateDecodingStrategy = .formatted(dateFormatter)
    }
    print(item1)
} catch {
    print("Error: \(error)")
}

print("\n========================")
let item2 = Item(discardingAnErrorFrom: dictionary)
print(String(describing: item2))

print("\n========================")
let item3 = Item(discardingAnErrorFrom: dictionary, printError: true)
print(String(describing: item3))

print("\n========================")
let item4 = Item(discardingAnErrorFrom: dictionary){ decoder in
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
    decoder.dateDecodingStrategy = .formatted(dateFormatter)
}
print(String(describing: item4))

Usage log

Item(id: 1, name: "Item", isActive: false, date: 2019-08-06 10:55:00 +0000)

========================
nil

========================
Item decoding ERROR:
keyNotFound(CodingKeys(stringValue: "isActive", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"isActive\", intValue: nil) (\"isActive\").", underlyingError: nil))
nil

========================
Optional(__lldb_expr_5.Item(id: 1, name: "Item", isActive: false, date: 2019-08-06 10:55:00 +0000))

Solution 3

I adapted Chris Mitchelmore's answer so that it is a failable initializer instead of throwing code. Makes it a bit handier in some cases.

extension Decodable {
    init?(from: Any) {
        guard let data = try? JSONSerialization.data(withJSONObject: from, options: .prettyPrinted) else { return nil }
        let decoder = JSONDecoder()
        guard let decoded = try? decoder.decode(Self.self, from: data) else { return nil }
        self = decoded
    }
}
Share:
14,241
Chris Mitchelmore
Author by

Chris Mitchelmore

I'm a technology consultant with a focus on mobile and iOS development. I work mainly with startups including UK App prize winners, building integration for Apple Pay launch partners and working for one of the fastest funded companies ever on seedr. Through work with these companies, and others, I've spent time in The Bakery, completed the Microsoft Ventures accelerator program and taught iOS application development at City University. On top of iOS and backend web development, I can provide mobile strategy and early stage startup advice to my clients. More about me

Updated on June 15, 2022

Comments

  • Chris Mitchelmore
    Chris Mitchelmore about 2 years

    Primarily my use case is to create an object using a dictionary: e.g.

    struct Person: Codable { let name: String }    
    let dictionary = ["name": "Bob"]
    let person = Person(from: dictionary)    
    

    I would like to avoid writing custom implementations and want to be as efficient as possible.