171

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": "john.doe@example.com",
  "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?

Anton Tropashko
  • 5,486
  • 5
  • 41
  • 66

16 Answers16

150

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)
    }
}
mfaani
  • 33,269
  • 19
  • 164
  • 293
loudmouth
  • 1,946
  • 1
  • 12
  • 16
  • Interesting, I'll try this gist and will update the result to you @loudmouth – Pitiphong Phongpattranont Sep 05 '17 at 08:05
  • @PitiphongPhongpattranont did this code work out for you? – loudmouth Sep 21 '17 at 12:57
  • I would say yes. I refactor this snippet a little bit but your main idea works really great. Thank you – Pitiphong Phongpattranont Sep 21 '17 at 19:14
  • Why set the value to `true` when decoding a nil for a key? – dan Dec 21 '17 at 17:00
  • @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 ;-) – loudmouth Dec 26 '17 at 00:27
  • I'm not seeing how this solution works: I get infinite recursion in `UnkeyedDecodingContainer`'s `decode(_ type: Array.Type) throws -> Array` which calls itself in the last `if` statement. – Jon Brooks Jan 12 '18 at 17:52
  • I deleted my previous comments because it was my fault that my code wasn't working. Now it works fine. Thanks you so much. – WedgeSparda Jan 18 '18 at 14:44
  • 1
    @JonBrooks the last condition in the in `UnkeyedDecodingContainer`'s `decode(_ type: Array.Type) throws -> Array` 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. – loudmouth Jan 24 '18 at 10:59
  • @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 }` – chebur Mar 09 '18 at 11:39
  • 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. – loudmouth Mar 14 '18 at 15:52
  • 1
    @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`. – chebur Mar 14 '18 at 21:48
  • Got it @chebur! I'll revise soon! – loudmouth Apr 05 '18 at 09:12
  • 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: https://gist.github.com/jonbrooks/a2f0f19d8bcb00b51cf1b0567d06c720 – Jon Brooks Sep 17 '18 at 21:30
  • 2
    I found a fix: Instead of `} else if let nestedArray = try? decode(Array.self, forKey: key)` try: `} else if var nestedContainer = try? nestedUnkeyedContainer(), let nestedArray = try? nestedContainer.decode(Array.self) {` – Jon Brooks Sep 17 '18 at 21:47
  • 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.self)```` – Eli Burke May 01 '19 at 16:57
40

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.

zoul
  • 102,279
  • 44
  • 260
  • 354
  • 1
    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. – user3236716 Jul 22 '21 at 18:29
  • Would you show an example how to decode a string into your structure? –  Sep 08 '22 at 16:48
16

You can create metadata struct which conforms 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": "john.doe@example.com",
    "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)
}
LondonGuy
  • 10,778
  • 11
  • 79
  • 151
Suhit Patil
  • 11,748
  • 3
  • 50
  • 60
  • 41
    No I can't, since I don't know the structure of the `metadata` value. It can be any arbitrary object. – Pitiphong Phongpattranont Jun 17 '17 at 14:01
  • Do you mean it can be either Array or Dictionary type? – Suhit Patil Jun 18 '17 at 04:31
  • can you give example or add more explaination about metadata structure – Suhit Patil Jun 18 '17 at 05:16
  • 4
    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. – Pitiphong Phongpattranont Jun 18 '17 at 17:50
  • 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? } – Suhit Patil Jun 19 '17 at 18:06
  • 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. – David H Apr 20 '18 at 14:57
14

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)
Giuseppe Lanza
  • 3,519
  • 1
  • 18
  • 40
10

If you use SwiftyJSON to parse JSON, you can update to 4.1.0 which has Codable protocol support. Just declare metadata: JSON and you're all set.

import SwiftyJSON

struct Customer {
  let id: String
  let email: String
  let metadata: JSON
}
allen huang
  • 165
  • 1
  • 4
9

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.

  • 4
    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. – Reza Shirazian Jul 21 '17 at 05:39
  • @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 – Pitiphong Phongpattranont Jul 24 '17 at 05:05
  • 8
    currently this doesn't work. "Dictionary does not conform to Decodable because Any does not conform to Decodable" – mbuchetics Jul 24 '17 at 10:25
  • 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. – Pitiphong Phongpattranont Jul 24 '17 at 17:32
  • 4
    Doesn’t work for me as of Xcode 9 beta 5. Compiles, but blows up at runtime: _Dictionary does not conform to Decodable because Any does not conform to Decodable._ – zoul Aug 11 '17 at 11:34
  • @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 Aug 23 '17 at 08:49
  • 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 Aug 26 '17 at 09:00
  • 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" – harshit2811 Sep 29 '17 at 09:36
  • @harshit2811Please refer to the accepted answer for the proper workaround – Pitiphong Phongpattranont Oct 01 '17 at 08:43
2

I have written an article and repo that helps in adding [String: Any] support for Codable for decoding as well as encoding.

https://medium.com/nerd-for-tech/string-any-support-for-codable-4ba062ce62f2

This improves on decodable aspect and also add encodable support as solution given by in https://stackoverflow.com/a/46049763/9160905

what you will be able to achieve:

json:

enter image description here

sample code:

enter image description here

koen
  • 5,383
  • 7
  • 50
  • 89
1

You might have a look at BeyovaJSON

import BeyovaJSON

struct Customer: Codable {
  let id: String
  let email: String
  let metadata: JToken
}

//create a customer instance

customer.metadata = ["link_id": "linked-id","buy_count": 4]

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted 
print(String(bytes: try! encoder.encode(customer), encoding: .utf8)!)
canius
  • 93
  • 5
  • 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 :) – Vitor Hugo Schwaab Feb 28 '18 at 18:41
1

Here is more generic (not only [String: Any], but [Any] can decoded) and encapsulated approach (separate entity is used for that) inspired by @loudmouth answer.

Using it will look like:

extension Customer: Decodable {
  public init(from decoder: Decoder) throws {
    let selfContainer = try decoder.container(keyedBy: CodingKeys.self)
    id = try selfContainer.decode(.id)
    email = try selfContainer.decode(.email)
    let metadataContainer: JsonContainer = try selfContainer.decode(.metadata)
    guard let metadata = metadataContainer.value as? [String: Any] else {
      let context = DecodingError.Context(codingPath: [CodingKeys.metadata], debugDescription: "Expected '[String: Any]' for 'metadata' key")
      throw DecodingError.typeMismatch([String: Any].self, context)
    }
    self.metadata = metadata
  }

  private enum CodingKeys: String, CodingKey {
    case id, email, metadata
  }
}

JsonContainer is a helper entity we use to wrap decoding JSON data to JSON object (either array or dictionary) without extending *DecodingContainer (so it won't interfere with rare cases when a JSON object is not meant by [String: Any]).

struct JsonContainer {

  let value: Any
}

extension JsonContainer: Decodable {

  public init(from decoder: Decoder) throws {
    if let keyedContainer = try? decoder.container(keyedBy: Key.self) {
      var dictionary = [String: Any]()
      for key in keyedContainer.allKeys {
        if let value = try? keyedContainer.decode(Bool.self, forKey: key) {
          // Wrapping numeric and boolean types in `NSNumber` is important, so `as? Int64` or `as? Float` casts will work
          dictionary[key.stringValue] = NSNumber(value: value)
        } else if let value = try? keyedContainer.decode(Int64.self, forKey: key) {
          dictionary[key.stringValue] = NSNumber(value: value)
        } else if let value = try? keyedContainer.decode(Double.self, forKey: key) {
          dictionary[key.stringValue] = NSNumber(value: value)
        } else if let value = try? keyedContainer.decode(String.self, forKey: key) {
          dictionary[key.stringValue] = value
        } else if (try? keyedContainer.decodeNil(forKey: key)) ?? false {
          // NOP
        } else if let value = try? keyedContainer.decode(JsonContainer.self, forKey: key) {
          dictionary[key.stringValue] = value.value
        } else {
          throw DecodingError.dataCorruptedError(forKey: key, in: keyedContainer, debugDescription: "Unexpected value for \(key.stringValue) key")
        }
      }
      value = dictionary
    } else if var unkeyedContainer = try? decoder.unkeyedContainer() {
      var array = [Any]()
      while !unkeyedContainer.isAtEnd {
        let container = try unkeyedContainer.decode(JsonContainer.self)
        array.append(container.value)
      }
      value = array
    } else if let singleValueContainer = try? decoder.singleValueContainer() {
      if let value = try? singleValueContainer.decode(Bool.self) {
        self.value = NSNumber(value: value)
      } else if let value = try? singleValueContainer.decode(Int64.self) {
        self.value = NSNumber(value: value)
      } else if let value = try? singleValueContainer.decode(Double.self) {
        self.value = NSNumber(value: value)
      } else if let value = try? singleValueContainer.decode(String.self) {
        self.value = value
      } else if singleValueContainer.decodeNil() {
        value = NSNull()
      } else {
        throw DecodingError.dataCorruptedError(in: singleValueContainer, debugDescription: "Unexpected value")
      }
    } else {
      let context = DecodingError.Context(codingPath: [], debugDescription: "Invalid data format for JSON")
      throw DecodingError.dataCorrupted(context)
    }
  }

  private struct Key: CodingKey {
    var stringValue: String

    init?(stringValue: String) {
      self.stringValue = stringValue
    }

    var intValue: Int?

    init?(intValue: Int) {
      self.init(stringValue: "\(intValue)")
      self.intValue = intValue
    }
  }
}

Note that numberic and boolean types are backed by NSNumber, else something like this won't work:

if customer.metadata["keyForInt"] as? Int64 { // as it always will be nil
Alexey Kozhevnikov
  • 4,249
  • 1
  • 21
  • 29
  • 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? – Michał Ziobro Feb 20 '18 at 09:24
  • @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. – Alexey Kozhevnikov Feb 20 '18 at 10:23
  • I would like to have only some properties with customized decoding and the rest with standard default decoding – Michał Ziobro Feb 20 '18 at 11:27
  • @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. – Alexey Kozhevnikov Feb 20 '18 at 12:28
1

I have made a pod to facilitate the way the decoding + encoding [String: Any], [Any]. And this provides encode or decode the optional properties, here https://github.com/levantAJ/AnyCodable

pod 'DynamicCodable', '1.0'

How to use it:

import DynamicCodable

struct YourObject: Codable {
    var dict: [String: Any]
    var array: [Any]
    var optionalDict: [String: Any]?
    var optionalArray: [Any]?

    enum CodingKeys: String, CodingKey {
        case dict
        case array
        case optionalDict
        case optionalArray
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        dict = try values.decode([String: Any].self, forKey: .dict)
        array = try values.decode([Any].self, forKey: .array)
        optionalDict = try values.decodeIfPresent([String: Any].self, forKey: .optionalDict)
        optionalArray = try values.decodeIfPresent([Any].self, forKey: .optionalArray)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(dict, forKey: .dict)
        try container.encode(array, forKey: .array)
        try container.encodeIfPresent(optionalDict, forKey: .optionalDict)
        try container.encodeIfPresent(optionalArray, forKey: .optionalArray)
    }
}
Tai Le
  • 8,530
  • 5
  • 41
  • 34
1

Details

  • Xcode 12.0.1 (12A7300)
  • Swift 5.3

Based on Tai Le library

// code from: https://github.com/levantAJ/AnyCodable/blob/master/AnyCodable/DecodingContainer%2BAnyCollection.swift

private
struct AnyCodingKey: CodingKey {
    let stringValue: String
    private (set) var intValue: Int?
    init?(stringValue: String) { self.stringValue = stringValue }
    init?(intValue: Int) {
        self.intValue = intValue
        stringValue = String(intValue)
    }
}

extension KeyedDecodingContainer {

    private
    func decode(_ type: [Any].Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> [Any] {
        var values = try nestedUnkeyedContainer(forKey: key)
        return try values.decode(type)
    }

    private
    func decode(_ type: [String: Any].Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> [String: Any] {
        try nestedContainer(keyedBy: AnyCodingKey.self, forKey: key).decode(type)
    }

    func decode(_ type: [String: Any].Type) throws -> [String: Any] {
        var dictionary: [String: Any] = [:]
        for key in allKeys {
            if try decodeNil(forKey: key) {
                dictionary[key.stringValue] = NSNull()
            } else if let bool = try? decode(Bool.self, forKey: key) {
                dictionary[key.stringValue] = bool
            } else if let string = try? decode(String.self, forKey: key) {
                dictionary[key.stringValue] = string
            } else if let int = try? decode(Int.self, forKey: key) {
                dictionary[key.stringValue] = int
            } else if let double = try? decode(Double.self, forKey: key) {
                dictionary[key.stringValue] = double
            } else if let dict = try? decode([String: Any].self, forKey: key) {
                dictionary[key.stringValue] = dict
            } else if let array = try? decode([Any].self, forKey: key) {
                dictionary[key.stringValue] = array
            }
        }
        return dictionary
    }
}

extension UnkeyedDecodingContainer {
    mutating func decode(_ type: [Any].Type) throws -> [Any] {
        var elements: [Any] = []
        while !isAtEnd {
            if try decodeNil() {
                elements.append(NSNull())
            } else if let int = try? decode(Int.self) {
                elements.append(int)
            } else if let bool = try? decode(Bool.self) {
                elements.append(bool)
            } else if let double = try? decode(Double.self) {
                elements.append(double)
            } else if let string = try? decode(String.self) {
                elements.append(string)
            } else if let values = try? nestedContainer(keyedBy: AnyCodingKey.self),
                let element = try? values.decode([String: Any].self) {
                elements.append(element)
            } else if var values = try? nestedUnkeyedContainer(),
                let element = try? values.decode([Any].self) {
                elements.append(element)
            }
        }
        return elements
    }
}

Solution

struct DecodableDictionary: Decodable {
    typealias Value = [String: Any]
    let dictionary: Value?
    init(from decoder: Decoder) throws {
        dictionary = try? decoder.container(keyedBy: AnyCodingKey.self).decode(Value.self)
    }
}

Usage

struct Model: Decodable {
    let num: Double?
    let flag: Bool?
    let dict: DecodableDictionary?
    let dict2: DecodableDictionary?
    let dict3: DecodableDictionary?
}

let data = try! JSONSerialization.data(withJSONObject: dictionary)
let object = try JSONDecoder().decode(Model.self, from: data)
print(object.dict?.dictionary)      // prints [String: Any]
print(object.dict2?.dictionary)     // prints nil
print(object.dict3?.dictionary)     // prints nil
Vasily Bodnarchuk
  • 24,482
  • 9
  • 132
  • 127
0

I used some of the answers on this topic to get the simplest solution possible for me. My problem is that I was receiving a [String: Any] type dictionary, but I could very well work with a [String: String] transforming every other Any value in String. So this is my solution:

struct MetadataType: Codable {
    let value: String?

    private init(_ value: String?) {
        self.value = value
    }

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

        if let decodedValue = try? container.decode(Int.self) {
            self.init(String(decodedValue))
        } else if let decodedValue = try? container.decode(Double.self) {
            self.init(String(decodedValue))
        } else if let decodedValue = try? container.decode(Bool.self) {
            self.init(String(decodedValue))
        } else if let decodedValue = try? container.decode(String.self) {
            self.init(decodedValue)
        } else {
            self.init(nil)
        }
    }
}

And when declaring my dictionary, I use

let userInfo: [String: MetadataType]
Tassio Marques
  • 81
  • 1
  • 10
-1

The easiest and suggested way is to create separate model for each dictionary or model that is in JSON.

Here is what I do

//Model for dictionary **Metadata**

struct Metadata: Codable {
    var link_id: String?
    var buy_count: Int?
}  

//Model for dictionary **Customer**

struct Customer: Codable {
   var object: String?
   var id: String?
   var email: String?
   var metadata: Metadata?
}

//Here is our decodable parser that decodes JSON into expected model

struct CustomerParser {
    var customer: Customer?
}

extension CustomerParser: Decodable {

//keys that matches exactly with JSON
enum CustomerKeys: String, CodingKey {
    case object = "object"
    case id = "id"
    case email = "email"
    case metadata = "metadata"
}

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CustomerKeys.self) // defining our (keyed) container

    let object: String = try container.decode(String.self, forKey: .object) // extracting the data
    let id: String = try container.decode(String.self, forKey: .id) // extracting the data
    let email: String = try container.decode(String.self, forKey: .email) // extracting the data

   //Here I have used metadata model instead of dictionary [String: Any]
    let metadata: Metadata = try container.decode(Metadata.self, forKey: .metadata) // extracting the data

    self.init(customer: Customer(object: object, id: id, email: email, metadata: metadata))

    }
}

Usage:

  if let url = Bundle.main.url(forResource: "customer-json-file", withExtension: "json") {
        do {
            let jsonData: Data =  try Data(contentsOf: url)
            let parser: CustomerParser = try JSONDecoder().decode(CustomerParser.self, from: jsonData)
            print(parser.customer ?? "null")

        } catch {

        }
    }

**I have used optional to be in safe side while parsing, can be changed as needed.

Read more on this topic

minhazur
  • 4,928
  • 3
  • 25
  • 27
  • 1
    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 Apr 20 '18 at 14:54
-1

decode using decoder and coding keys

public let dataToDecode: [String: AnyDecodable]

enum CodingKeys: CodingKey {
    case dataToDecode
}

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.dataToDecode = try container.decode(Dictionary<String, AnyDecodable>.self, forKey: .dataToDecode) 
}    
Ashim Dahal
  • 1,097
  • 13
  • 15
-1

This will work

public struct AnyDecodable: Decodable {
    public let value: Any

    public init<T>(_ value: T?) {
        self.value = value ?? ()
    }
}

let contentDecodable = try values.decodeIfPresent(AnyDecodable.self, forKey: .content)
Mahendra Y
  • 1,941
  • 20
  • 26
-2
extension ViewController {

    func swiftyJson(){
        let url = URL(string: "https://itunes.apple.com/search?term=jack+johnson")
        //let url = URL(string: "http://makani.bitstaging.in/api/business/businesses_list")

        Alamofire.request(url!, method: .get, parameters: nil).responseJSON { response in
            var arrayIndexes = [IndexPath]()
            switch(response.result) {
            case .success(_):

                let data = response.result.value as! [String : Any]

                if let responseData =  Mapper<DataModel>().map(JSON: data) {
                    if responseData.results!.count > 0{
                        self.arrayExploreStylistList = []
                    }
                    for i in 0..<responseData.results!.count{
                        arrayIndexes.append(IndexPath(row: self.arrayExploreStylistList.count + i, section: 0))
                    }
                    self.arrayExploreStylistList.append(contentsOf: responseData.results!)

                    print(arrayIndexes.count)

                }

                //                    if let arrNew = data["results"] as? [[String : Any]]{
                //                        let jobData = Mapper<DataModel>().mapArray(JSONArray: arrNew)
                //                        print(jobData)
                //                        self.datamodel = jobData
                //                    }
                self.tblView.reloadData()
                break

            case .failure(_):
                print(response.result.error as Any)
                break

            }
        }

    }
}
pkamb
  • 33,281
  • 23
  • 160
  • 191
Hiren
  • 260
  • 3
  • 3