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?
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)
}
}
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
.
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.
You can create metadata struct which confirms to Codable
protocol and use Decodable
class to create object 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: Codable {
let object: String
let id: String
let email: String
let metadata: Metadata
}
struct Metadata: Codable {
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)
}
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)
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)!)
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
}
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)
}
}
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.
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
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
}
}
}
}
What you want go against the design of Codable
. The idea behind Codable
is to provide a mechanism to archive and unarchive data in a type-safe manner. This means you must define the properties and their data types before hand. I can think of 2 solutions to your problem:
1. List all potential metadata keys
Oftentimes, if you reach deep enough into the API's documentation, you will find the full list of all potential metadata keys. Define a Metadata
struct, with these keys as optional properties:
struct Customer: Decodable {
struct Metadata: Decodable {
var linkId: String?
var buyCount: Int?
var somethingElse: Int?
private enum CodingKeys: String, CodingKey {
case linkId = "link_id"
case buyCount = "buy_count"
case somethingElse = "something_else"
}
}
var object: String
var id: String
var email: String
var metadata: Metadata
}
let customer = try! JSONDecoder().decode(Customer.self, from: jsonData)
print(customer.metadata)
I can see that the Swift designers would have preferred this approach.
2. Combine Decodable and JSONSerialization
JSONSerialization
offers great dynamism in the trade off for type safety. You can definitely mix it up with Decodable
, whose design philosophy is just the opposite:
struct Customer {
private struct RawCustomer: Decodable {
var object: String
var id: String
var email: String
}
var object: String
var id: String
var email: String
var metadata: [String: AnyObject]
init(jsonData: Data) throws {
let rawCustomer = try JSONDecoder().decode(RawCustomer.self, from: jsonData)
object = rawCustomer.object
id = rawCustomer.id
email = rawCustomer.email
let jsonObject = try JSONSerialization.jsonObject(with: jsonData)
if let dict = jsonObject as? [String: AnyObject],
let metadata = dict["metadata"] as? [String: AnyObject]
{
self.metadata = metadata
} else {
self.metadata = [String: AnyObject]()
}
}
}
let customer = try! Customer(jsonData: jsonData)
print(customer.metadata)
来源:https://stackoverflow.com/questions/44603248/how-to-decode-a-property-with-type-of-json-dictionary-in-swift-4-decodable-proto