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

122,014

Solution 1

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)
    }
}

Solution 2

I have played with this problem, too, and finally wrote a simple library for working with “generic JSON” types. (Where “generic” means “with no structure known in advance”.) Main point is representing the generic JSON with a concrete type:

public enum JSON {
    case string(String)
    case number(Float)
    case object([String:JSON])
    case array([JSON])
    case bool(Bool)
    case null
}

This type can then implement Codable and Equatable.

Solution 3

You can create metadata struct which confirms to Decodable protocol and use JSONDecoder class to create object from data by using decode method like below

let json: [String: Any] = [
    "object": "customer",
    "id": "4yq6txdpfadhbaqnwp3",
    "email": "[email protected]",
    "metadata": [
        "link_id": "linked-id",
        "buy_count": 4
    ]
]

struct Customer: Decodable {
    let object: String
    let id: String
    let email: String
    let metadata: Metadata
}

struct Metadata: Decodable {
    let link_id: String
    let buy_count: Int
}

let data = try JSONSerialization.data(withJSONObject: json, options: .prettyPrinted)

let decoder = JSONDecoder()
do {
    let customer = try decoder.decode(Customer.self, from: data)
    print(customer)
} catch {
    print(error.localizedDescription)
}

Solution 4

I came with a slightly different solution.

Let's suppose we have something more than a simple [String: Any] to parse were Any might be an array or a nested dictionary or a dictionary of arrays.

Something like this:

var json = """
{
  "id": 12345,
  "name": "Giuseppe",
  "last_name": "Lanza",
  "age": 31,
  "happy": true,
  "rate": 1.5,
  "classes": ["maths", "phisics"],
  "dogs": [
    {
      "name": "Gala",
      "age": 1
    }, {
      "name": "Aria",
      "age": 3
    }
  ]
}
"""

Well, this is my solution:

public struct AnyDecodable: Decodable {
  public var value: Any

  private struct CodingKeys: CodingKey {
    var stringValue: String
    var intValue: Int?
    init?(intValue: Int) {
      self.stringValue = "\(intValue)"
      self.intValue = intValue
    }
    init?(stringValue: String) { self.stringValue = stringValue }
  }

  public init(from decoder: Decoder) throws {
    if let container = try? decoder.container(keyedBy: CodingKeys.self) {
      var result = [String: Any]()
      try container.allKeys.forEach { (key) throws in
        result[key.stringValue] = try container.decode(AnyDecodable.self, forKey: key).value
      }
      value = result
    } else if var container = try? decoder.unkeyedContainer() {
      var result = [Any]()
      while !container.isAtEnd {
        result.append(try container.decode(AnyDecodable.self).value)
      }
      value = result
    } else if let container = try? decoder.singleValueContainer() {
      if let intVal = try? container.decode(Int.self) {
        value = intVal
      } else if let doubleVal = try? container.decode(Double.self) {
        value = doubleVal
      } else if let boolVal = try? container.decode(Bool.self) {
        value = boolVal
      } else if let stringVal = try? container.decode(String.self) {
        value = stringVal
      } else {
        throw DecodingError.dataCorruptedError(in: container, debugDescription: "the container contains nothing serialisable")
      }
    } else {
      throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Could not serialise"))
    }
  }
}

Try it using

let stud = try! JSONDecoder().decode(AnyDecodable.self, from: jsonData).value as! [String: Any]
print(stud)

Solution 5

When I found the old answer, I only tested a simple JSON object case but not an empty one which will cause a runtime exception like @slurmomatic and @zoul found. Sorry for this issue.

So I try another way by having a simple JSONValue protocol, implement the AnyJSONValue type erasure struct and use that type instead of Any. Here's an implementation.

public protocol JSONType: Decodable {
    var jsonValue: Any { get }
}

extension Int: JSONType {
    public var jsonValue: Any { return self }
}
extension String: JSONType {
    public var jsonValue: Any { return self }
}
extension Double: JSONType {
    public var jsonValue: Any { return self }
}
extension Bool: JSONType {
    public var jsonValue: Any { return self }
}

public struct AnyJSONType: JSONType {
    public let jsonValue: Any

    public init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()

        if let intValue = try? container.decode(Int.self) {
            jsonValue = intValue
        } else if let stringValue = try? container.decode(String.self) {
            jsonValue = stringValue
        } else if let boolValue = try? container.decode(Bool.self) {
            jsonValue = boolValue
        } else if let doubleValue = try? container.decode(Double.self) {
            jsonValue = doubleValue
        } else if let doubleValue = try? container.decode(Array<AnyJSONType>.self) {
            jsonValue = doubleValue
        } else if let doubleValue = try? container.decode(Dictionary<String, AnyJSONType>.self) {
            jsonValue = doubleValue
        } else {
            throw DecodingError.typeMismatch(JSONType.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Unsupported JSON tyep"))
        }
    }
}

And here is how to use it when decoding

metadata = try container.decode ([String: AnyJSONValue].self, forKey: .metadata)

The problem with this issue is that we must call value.jsonValue as? Int. We need to wait until Conditional Conformance land in Swift, that would solve this problem or at least help it to be better.


[Old Answer]

I post this question on the Apple Developer forum and it turns out it is very easy.

I can do

metadata = try container.decode ([String: Any].self, forKey: .metadata)

in the initializer.

It was my bad to miss that in the first place.

Share:
122,014

Related videos on Youtube

Pitiphong Phongpattranont
Author by

Pitiphong Phongpattranont

Updated on April 03, 2022

Comments

  • Pitiphong Phongpattranont
    Pitiphong Phongpattranont over 2 years

    Let's say I have Customer data type which contains a metadata property that can contains any JSON dictionary in the customer object

    struct Customer {
      let id: String
      let email: String
      let metadata: [String: Any]
    }
    

    {  
      "object": "customer",
      "id": "4yq6txdpfadhbaqnwp3",
      "email": "[email protected]",
      "metadata": {
        "link_id": "linked-id",
        "buy_count": 4
      }
    }
    

    The metadata property can be any arbitrary JSON map object.

    Before I can cast the property from a deserialized JSON from NSJSONDeserialization but with the new Swift 4 Decodable protocol, I still can't think of a way to do that.

    Do anyone know how to achieve this in Swift 4 with Decodable protocol?

  • Pitiphong Phongpattranont
    Pitiphong Phongpattranont about 7 years
    No I can't, since I don't know the structure of the metadata value. It can be any arbitrary object.
  • Suhit Patil
    Suhit Patil about 7 years
    Do you mean it can be either Array or Dictionary type?
  • Suhit Patil
    Suhit Patil about 7 years
    can you give example or add more explaination about metadata structure
  • Pitiphong Phongpattranont
    Pitiphong Phongpattranont about 7 years
    The value of metadata can be any JSON object. So it can be empty dictionary or any dictionary. "metadata": {} "metadata": { user_id: "id" } "metadata": { preference: { shows_value: true, language: "en" } } etc.
  • Suhit Patil
    Suhit Patil about 7 years
    one possible option would be to use all the params in metadata struct as optionals and list all the possible values in metadata struct like struct metadata { var user_id: String? var preference: String? }
  • Reza Shirazian
    Reza Shirazian almost 7 years
    Could post the link to question on Apple Developer. Any does not conform to Decodable so I'm not sure how this is the correct answer.
  • Pitiphong Phongpattranont
    Pitiphong Phongpattranont almost 7 years
    @RezaShirazian That's what I thought in the first place. But it turns out that Dictionary conforms to Encodable when its keys conforms to Hashable and not depend on its values. You can open the Dictionary header and see that by yourself. extension Dictionary : Encodable where Key : Hashable extension Dictionary : Decodable where Key : Hashable forums.developer.apple.com/thread/80288#237680
  • mbuchetics
    mbuchetics almost 7 years
    currently this doesn't work. "Dictionary<String, Any> does not conform to Decodable because Any does not conform to Decodable"
  • Pitiphong Phongpattranont
    Pitiphong Phongpattranont almost 7 years
    Turns out it works. I'm using it in my code. You need to understand that there is no way to express the requirement that "Value of Dictionary must conforms to Decodable protocol in order to make the Dictionary to conform the Decodable protocol" now. That's the "Conditional Conformance" which is not yet implemented in Swift 4 I think it's ok for now since there are lots of limitation in the Swift Type System (and Generics). So this works for now but when the Swift Type System improve in the future (especially when the Conditional Conformance is implemented), this shouldn't work.
  • zoul
    zoul almost 7 years
    Doesn’t work for me as of Xcode 9 beta 5. Compiles, but blows up at runtime: Dictionary<String, Any> does not conform to Decodable because Any does not conform to Decodable.
  • Pitiphong Phongpattranont
    Pitiphong Phongpattranont almost 7 years
    @zoul it still works for me in beta 6 both on compile and at runtime. Btw contents in that property is just a plain JSON data type
  • Pitiphong Phongpattranont
    Pitiphong Phongpattranont almost 7 years
    I just found the problem that you talked about with an empty JSON and have updated my new solution. Sorry for the case I missed.
  • Pitiphong Phongpattranont
    Pitiphong Phongpattranont almost 7 years
    Interesting, I'll try this gist and will update the result to you @loudmouth
  • loudmouth
    loudmouth almost 7 years
    @PitiphongPhongpattranont did this code work out for you?
  • Pitiphong Phongpattranont
    Pitiphong Phongpattranont almost 7 years
    I would say yes. I refactor this snippet a little bit but your main idea works really great. Thank you
  • harshit2811
    harshit2811 almost 7 years
    Doesn't work either for me on final release of XCODE 9 and swift 4. I have tried with both [String: AnyObject].self and [String: Any].self . Same error " does not conform to Decodable"
  • Pitiphong Phongpattranont
    Pitiphong Phongpattranont almost 7 years
    @harshit2811Please refer to the accepted answer for the proper workaround
  • dan
    dan over 6 years
    Why set the value to true when decoding a nil for a key?
  • loudmouth
    loudmouth over 6 years
    @dan I think you're correct to ask this question: I think the condition for decoding nil should simply be skipped as storing nil in a dictionary is of course a no-no and trying to extract a non-existent value for a key will return nil anyway. I'll edit my answer now ;-)
  • Jon Brooks
    Jon Brooks over 6 years
    I'm not seeing how this solution works: I get infinite recursion in UnkeyedDecodingContainer's decode(_ type: Array<Any>.Type) throws -> Array<Any> which calls itself in the last if statement.
  • WedgeSparda
    WedgeSparda over 6 years
    I deleted my previous comments because it was my fault that my code wasn't working. Now it works fine. Thanks you so much.
  • loudmouth
    loudmouth over 6 years
    @JonBrooks the last condition in the in UnkeyedDecodingContainer's decode(_ type: Array<Any>.Type) throws -> Array<Any> is checking for a nested array. So if you have a data structure that looks like the following: [true, 452.0, ["a", "b", "c"] ] It would pull the nested ["a", "b", "c"] array. The decode method of an UnkeyedDecodingContainer "pops" off the element from the container. It shouldn't cause infinite recursion.
  • Michał Ziobro
    Michał Ziobro over 6 years
    Can I decode only chosen properties and leave other decoded automatically as I have 15 properties that suffice autoDecoding and maybe 3 that needs some custom decoding handling?
  • Alexey Kozhevnikov
    Alexey Kozhevnikov over 6 years
    @MichałZiobro Do you want part of data decoded into JSON object and part of it decoded into separate instance variables? Or you are asking about writing partial decoding initializer just for part of the object (and it does not have anything in common with JSON like structure)? To my knowledge, an answer to the first question is yes, to the second is no.
  • Michał Ziobro
    Michał Ziobro over 6 years
    I would like to have only some properties with customized decoding and the rest with standard default decoding
  • Alexey Kozhevnikov
    Alexey Kozhevnikov over 6 years
    @MichałZiobro If I understand you right it's not possible. Anyway, your question is not relevant to the current SO question and worth a separate one.
  • Vitor Hugo Schwaab
    Vitor Hugo Schwaab over 6 years
    Ooh, really nice. Using it to receive a generic JSON as JToken, appending some values and returning to the server. Very good indeed. That is awesome work you've done :)
  • chebur
    chebur over 6 years
    @loudmouth would you consider to fix all decodeIfPresent methods by adding a check if the key is actually exists by still has null value. For example: guard let isNil = try? decodeNil(forKey: key), !isNil else { return nil }
  • loudmouth
    loudmouth over 6 years
    Hey @chebur are you simply saying that decodeNil should be added as check at the end? I'm not sure what the goal would be here as storing nil in a Dictionary is not possible. You'll get a crash with an error like error: nil is not compatible with expected dictionary value type 'Any' If a value is nil in your JSON, you're best ignoring it as attempting to access the value for the dictionary key will return nil anyway.
  • chebur
    chebur over 6 years
    @loudmouth it is possible to have nil values for keys in json: {"array": null}. So your guard contains(key) will pass but it will crash few lines later when trying to decode null value for key "array". So it's better to add one more condition to check if the value is actually not null before calling decode.
  • loudmouth
    loudmouth about 6 years
    Got it @chebur! I'll revise soon!
  • David H
    David H about 6 years
    Your answer is the appropriate one for Swift 4.1 for sure and the first line of your post is dead on! Assuming the data is coming from a web service. you can model simple nested objects then use dot syntax to grab each. See suhit's answer below.
  • David H
    David H about 6 years
    If you know the structure of the data you got, then yes, this is the appropriate solution! Very nice! If it could vary, then you can try a couple of decodes to find the one that hopefully works.
  • Jon Brooks
    Jon Brooks almost 6 years
    Sorry for the delayed response. I still get infinite recursion with the code above, and can easily see the problem (as I described above). I'm curious whether all the upvotes have tested this case... Try the test case in this gist: gist.github.com/jonbrooks/a2f0f19d8bcb00b51cf1b0567d06c720
  • Jon Brooks
    Jon Brooks almost 6 years
    I found a fix: Instead of } else if let nestedArray = try? decode(Array<Any>.self, forKey: key) try: } else if var nestedContainer = try? nestedUnkeyedContainer(), let nestedArray = try? nestedContainer.decode(Array<Any>.self) {
  • Leonid Usov
    Leonid Usov over 5 years
    I don't know why this answer was downvoted. It's totally valid and solves the issue.
  • Michał Ziobro
    Michał Ziobro over 5 years
    It seems to be good for migration from SwiftyJSON to Decodable
  • Eli Burke
    Eli Burke about 5 years
    I was seeing infinite recursion on an array containing a dictionary [ { key : value } ]. @JonBrooks suggestion fixed it, but created a nested array where there wasn't one. I solved by extracting the array using nestedUnkeyedContainer in my Codable initializer: var nc = try unkeyedContainer.nestedUnkeyedContainer() results = try nestedContainer.decode(Array<Any>.self)
  • llamacorn
    llamacorn about 5 years
    This doesn't solve how to then parse the metadata json which was the original problem.
  • user3236716
    user3236716 almost 3 years
    This is a very elegant solution. It's extremely concise, works well, and is not hacky like some of the other answers. My only addition would be to swap out number for separate integer and floating point types. Technically all numbers are floats in JS, but it's more efficient and cleaner to decode integers as integers in swift.
  • Mary Doe
    Mary Doe over 2 years
    how will this decode an array?
  • WikipediaBrown
    WikipediaBrown over 2 years
    You need to define AnyDecodable.
  • Mr Spring
    Mr Spring about 2 years
    AnyDecodable isn't conforming Decodable