2

Suppose I have the following struct definition and dictionary:

struct Point: Codable {
    let x: Int
    let y: Int
}

let pointDictionary = [ "x": 0, "y": 1]

// Inital example was too easy so also we might have
struct Score: Codable {
    let points: Int
    let name: String
}

let scoreDictionary: [String: Any] = [
    "points": 10,
    "name": "iOSDevZone"
]

Is there a way, without a roundtrip through JSON or PList, to populate an instance of the struct Point from pointDictionary?

What I've Tried

I've looked at the Apple docs and can't really find a way.

To be clear, I understand I could write a custom initializer that takes a Dictionary (as I was submitting this question the system matched with an answer that illustrates this), but that's not what I am asking. (And this is not practical in my real situation, this is purely a demonstrative example).

I am asking, given a [String:Any] Dictionary where the keys match the property names of a struct and the values are convertible to the types of the properties, is there a way of leveraging Decodable to initialize the struct?

Why a Dictionary init is not desirable Since most responses have been: "Why not implement a dictionary init?"

There are lots of structs and many properties, the dictionaries come from processing bad JSON (that I have no control over).

idz
  • 12,825
  • 1
  • 29
  • 40
  • you could try: `let thePoint = Point(x: pointDictionary["x"] ?? 0, y: pointDictionary["y"] ?? 0)` – workingdog support Ukraine Apr 15 '23 at 06:57
  • I actually think an init that takes a dictionary would be a good solution in this case, make it a failable init in case the conversion can’t be done. – Joakim Danielson Apr 15 '23 at 07:12
  • As to the question of using decoding (without json) here I am pretty sure the answer is no. – Joakim Danielson Apr 15 '23 at 07:18
  • @JoakimDanielson I am skeptical too. I can see that such a Decoder could be written; but before I do I want to make sure I am not missing something obvious! – idz Apr 15 '23 at 07:29
  • `Codable` does not support `Any`, not at all. It might be possible with a `[String:Int]` dictionary – vadian Apr 15 '23 at 07:33
  • @vadian I understand that, I suppose I was hoping that there was some adapter or encoder I did not know about. My real types are not as simple. – idz Apr 15 '23 at 07:37
  • Regarding your last edit, without seeing the actual data it's hard to give some definitive advice. Perhaps work with types that has optional properties, perhaps try to decode into different types if the "bad JSON" is consistent in how it is bad. – Joakim Danielson Apr 15 '23 at 07:44
  • 1
    @JoakimDanielson I appreciate your time, but it seems like the consensus is that I am not missing anything, i.e. there is no inbuilt or straightforward way of taking a [String:Any] -> Type? via `Codable`. Obviously such a thing can be written, but I need to see if navigating the "bad JSON" to the bits I care about is worth it. – idz Apr 15 '23 at 07:50
  • Yes and if a consensus has been reached then maybe it’s best to close/remove the question? – Joakim Danielson Apr 15 '23 at 10:55
  • Can you describe the "bad JSON" a little more? If you just want to extract some json-path and only decode that piece, ignoring everything else, that's a little bit easier than the answer I posted here. – Rob Napier Apr 15 '23 at 19:49
  • @RobNapier, in retrospect I should have been clearer about that. It's JSON that varies a lot based on field values. I can't share explicit examples here, but to give you the idea, imagine if the objects had "type" and "value" fields and the keys likely to be present depend on the type. So bad in this sense means "maps poorly onto a strongly typed type hierarchy". Your answer works well for this situation; I can deserialize the JSON to a dictionary/array hierarchy, conditionally navigate to bits I care about, and map them onto structure at a level where it makes sense. Thank you! – idz Apr 17 '23 at 18:24
  • @JaokimDanielson, I left the question open in the hope that someone might have some clever way of implementing this without writing a `Decoder` from scratch. I'm glad I did. I think Rob's answer is very useful. – idz Apr 17 '23 at 18:26
  • 1
    You may want to look at deserializing to JSON types rather than dictionaries. They're more powerful. And you can apply the same approach to decode JSON types directly to Codables (using the Linux version of JSONDecoder: https://github.com/apple/swift-corelibs-foundation/blob/main/Sources/Foundation/JSONDecoder.swift). https://stackoverflow.com/questions/65901928/swift-jsonencoder-encoding-class-containing-a-nested-raw-json-object-literal/65902852#65902852 – Rob Napier Apr 17 '23 at 18:45
  • 1
    You may find this helpful as well: https://youtu.be/-k_vipGhugQ?t=1908 – Rob Napier Apr 17 '23 at 18:51
  • @RobNapier thank you! I think using the approach in the video for the fiddly bits will yield a far more elegant solution than using `JSONSerialization` which I had been doing. Then once I get to the more well behaved areas I can use `JSONDecoder`. I really appreciate you taking the time to point me in the right direction. – idz Apr 17 '23 at 18:57

2 Answers2

1

This is definitely possible because it's how Foundation does it on Darwin:

open func decode<T : Decodable>(_ type: T.Type, from data: Data) throws -> T {
    let topLevel: Any
    do {
       topLevel = try JSONSerialization.jsonObject(with: data, options: .fragmentsAllowed)
    } catch {
        throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: error))
    }

    let decoder = __JSONDecoder(referencing: topLevel, options: self.options)
    guard let value = try decoder.unbox(topLevel, as: type) else {
        throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: [], debugDescription: "The given data did not contain a top-level value."))
    }

    return value
}

You can pass Any to __JSONDecoder and it'll decode it. Unfortunately, __JSONDecoder is private. But it is also open source, so that's fixable. It's just tedious.

You need to copy roughly 1500 lines of __JSONDecoder implementation and a few supporting types, remove the "private" in front of __JSONDecoder, and then you can add an extension that does what you want:

extension JSONDecoder {
    fileprivate var options: _Options {
        return _Options(dateDecodingStrategy: dateDecodingStrategy,
                        dataDecodingStrategy: dataDecodingStrategy,
                        nonConformingFloatDecodingStrategy: nonConformingFloatDecodingStrategy,
                        keyDecodingStrategy: keyDecodingStrategy,
                        userInfo: userInfo)
    }

    func decode<T : Decodable>(_ type: T.Type, from topLevel: Any) throws -> T {
        let decoder = __JSONDecoder(referencing: topLevel, options: self.options)
        guard let value = try decoder.unbox(topLevel, as: type) else {
            throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: [], debugDescription: "The given data did not contain a top-level value."))
        }

        return value
    }
}

// And then it "just works":
let score = try JSONDecoder().decode(Score.self, from: scoreDictionary)
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
0

No.

The best solution Codable allows is to do what you've already mentioned—it can look like what you want, but not subvert the lack of direct API support.

import Foundation

public extension Decodable {
  /// - Parameter codingDictionary: `CodingKey`s paired with their values.
  init(codingDictionary: [String: Any]) throws {
    self = try JSONDecoder().decode(
      Self.self,
      from: JSONSerialization.data(withJSONObject: codingDictionary)
    )
  }
}
try Point(codingDictionary: pointDictionary)
try Score(codingDictionary: scoreDictionary)