21

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.

Chris Mitchelmore
  • 5,976
  • 3
  • 25
  • 33

3 Answers3

55

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

Chris Mitchelmore
  • 5,976
  • 3
  • 25
  • 33
  • 3
    What's the DateFormatter part for...? – Marty Oct 04 '18 at 09:02
  • @Marty: with Codable you can define your own date format in the Decorder in order to correct set objects date properties. – smukamuka Oct 17 '18 at 14:53
  • @smukamuka yes but in this particular case, what did it have to do with the question...? :) – Marty Oct 18 '18 at 07:15
  • 1
    Nothing! Just my particular problem had a date in and the fact that json serialisation automatically encodes dates and decodable doesn't confused me at first so I left it incase – Chris Mitchelmore Oct 18 '18 at 13:05
  • 1
    This is a great answer. If you are coming from https://stackoverflow.com/a/46329055/1453346 you should remove the date formatter lines, they break the decoding in that use case – lewis Mar 25 '20 at 12:12
  • I believe that `.prettyPrinted` only adds some unnecessary overhead, but its effect should be negligible because going through JSON is the major performance hit. – Alex Cohn Jan 21 '21 at 07:36
4

based on Chris Mitchelmore answer

Details

  • Xcode 14
  • Swift 5.6.1

Solution

import Foundation

enum DictionaryParsingError: Error {
    case jsonSerialization(Error)
    case decode(Error)
}

extension Decodable {

    static func from<Key>(dictionary: [Key: Any],
                          options: JSONSerialization.WritingOptions = [],
                          decoder: JSONDecoder) -> Result<Self, DictionaryParsingError> where Key: Hashable {
        let data: Data
        do {
            data = try JSONSerialization.data(withJSONObject: dictionary, options: options)
        } catch let error {
            return .failure(.jsonSerialization(error))
        }

        do {
            return .success(try decoder.decode(Self.self, from: data))
        } catch let error {
            return .failure(.decode(error))
        }
    }

    static func from<Key>(dictionary: [Key: Any],
                          options: JSONSerialization.WritingOptions = [],
                          singleUseDecoder configuration: (JSONDecoder) -> ()) -> Result<Self, DictionaryParsingError> where Key: Hashable {
        let decoder = JSONDecoder()
        configuration(decoder)
        return from(dictionary: dictionary, options: options, decoder: decoder)
    }
}

Usage

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

let dictionary = ["id": 1,
                  "name": "Item",
                  "date": "2019-08-06T06:55:00.000-04:00",
                  "is_active": false] as [String : Any]

print("========================")
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
decoder.dateDecodingStrategy = .formatted(dateFormatter)
switch Item.from(dictionary: dictionary, decoder: decoder) {
case let .failure(error): print("ERROR: \(error)")
case let .success(item): print(item)
}

print("\n========================")
let item2 = Item.from(dictionary: dictionary) { decoder in
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
    decoder.dateDecodingStrategy = .formatted(dateFormatter)
}
switch item2 {
case let .failure(error): print("ERROR: \(error)")
case let .success(item): print(item)
}

print("\n========================")
let item3 = Item.from(dictionary: [String:Any]()) { decoder in
    decoder.keyDecodingStrategy = .convertFromSnakeCase
            let dateFormatter = DateFormatter()
            dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
            decoder.dateDecodingStrategy = .formatted(dateFormatter)
}
switch item3 {
case let .failure(error): print("ERROR: \(error)")
case let .success(item): print(item)
}

Usage log

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

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

========================
ERROR: decode(Swift.DecodingError.keyNotFound(CodingKeys(stringValue: "id", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"id\", intValue: nil) (\"id\").", underlyingError: nil)))
Vasily Bodnarchuk
  • 24,482
  • 9
  • 132
  • 127
2

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
    }
}
Nico S.
  • 3,056
  • 1
  • 30
  • 64