How to decode a property with type of JSON dictionary in Swift [45] decodable protocol
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.
Related videos on Youtube
Pitiphong Phongpattranont
Updated on April 03, 2022Comments
-
Pitiphong Phongpattranont over 2 years
Let's say I have
Customer
data type which contains ametadata
property that can contains any JSON dictionary in the customer objectstruct 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 4Decodable
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 about 7 yearsNo I can't, since I don't know the structure of the
metadata
value. It can be any arbitrary object. -
Suhit Patil about 7 yearsDo you mean it can be either Array or Dictionary type?
-
Suhit Patil about 7 yearscan you give example or add more explaination about metadata structure
-
Pitiphong Phongpattranont about 7 yearsThe 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 about 7 yearsone 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 almost 7 yearsCould post the link to question on Apple Developer.
Any
does not conform toDecodable
so I'm not sure how this is the correct answer. -
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 almost 7 yearscurrently this doesn't work. "Dictionary<String, Any> does not conform to Decodable because Any does not conform to Decodable"
-
Pitiphong Phongpattranont almost 7 yearsTurns 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 almost 7 yearsDoesn’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 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 almost 7 yearsI 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 almost 7 yearsInteresting, I'll try this gist and will update the result to you @loudmouth
-
loudmouth almost 7 years@PitiphongPhongpattranont did this code work out for you?
-
Pitiphong Phongpattranont almost 7 yearsI would say yes. I refactor this snippet a little bit but your main idea works really great. Thank you
-
harshit2811 almost 7 yearsDoesn'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 almost 7 years@harshit2811Please refer to the accepted answer for the proper workaround
-
dan over 6 yearsWhy set the value to
true
when decoding a nil for a key? -
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 returnnil
anyway. I'll edit my answer now ;-) -
Jon Brooks over 6 yearsI'm not seeing how this solution works: I get infinite recursion in
UnkeyedDecodingContainer
'sdecode(_ type: Array<Any>.Type) throws -> Array<Any>
which calls itself in the lastif
statement. -
WedgeSparda over 6 yearsI deleted my previous comments because it was my fault that my code wasn't working. Now it works fine. Thanks you so much.
-
loudmouth over 6 years@JonBrooks the last condition in the in
UnkeyedDecodingContainer
'sdecode(_ 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. Thedecode
method of anUnkeyedDecodingContainer
"pops" off the element from the container. It shouldn't cause infinite recursion. -
Michał Ziobro over 6 yearsCan 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 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 over 6 yearsI would like to have only some properties with customized decoding and the rest with standard default decoding
-
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 over 6 yearsOoh, 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 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 over 6 yearsHey @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 storingnil
in a Dictionary is not possible. You'll get a crash with an error likeerror: nil is not compatible with expected dictionary value type 'Any'
If a value isnil
in your JSON, you're best ignoring it as attempting to access the value for the dictionary key will return nil anyway. -
chebur over 6 years@loudmouth it is possible to have nil values for keys in json:
{"array": null}
. So yourguard 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 callingdecode
. -
loudmouth about 6 yearsGot it @chebur! I'll revise soon!
-
David H about 6 yearsYour 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 about 6 yearsIf 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 almost 6 yearsSorry 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 almost 6 yearsI 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 over 5 yearsI don't know why this answer was downvoted. It's totally valid and solves the issue.
-
Michał Ziobro over 5 yearsIt seems to be good for migration from SwiftyJSON to Decodable
-
Eli Burke about 5 yearsI 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 about 5 yearsThis doesn't solve how to then parse the metadata json which was the original problem.
-
user3236716 almost 3 yearsThis 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 over 2 yearshow will this decode an array?
-
WikipediaBrown over 2 yearsYou need to define
AnyDecodable
. -
Mr Spring about 2 yearsAnyDecodable isn't conforming Decodable