How to decode a property with type of any arbitrary JSON dictionary in Swift 4 decodable protocol

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?

Accepted Reply

I think you're mixing up two different issues. One is whether the compiler is supposed to synthesize Codable conformance from your Customer class. The other is the correct type for a dictionary of "arbitrary" structure. (Note that because it's coming from JSON, it's not completely arbitrary. All the keys must actually be strings, and all valid JSON values are Codable-conforming.)


So, I tried writing out manually the code that the compiler is supposed to synthesize, which looks like this:


struct Customer: Codable {
     let id: String
     let email: String
     let metadata: [String: Any]
     enum CustomerKeys: String, CodingKey
     {
          case id, email, metadata
     }
     init (from decoder: Decoder) throws {
          let container =  try decoder.container (keyedBy: CustomerKeys.self)
          id = try container.decode (String.self, forKey: .id)
          email = try container.decode (String.self, forKey: .email)
          metadata = try container.decode ([String: Any].self, forKey: .metadata)
     }
     func encode (to encoder: Encoder) throws
     {
          var container = encoder.container (keyedBy: CustomerKeys.self)
          try container.encode (id, forKey: .id)
          try container.encode (email, forKey: .email)
          try container.encode (metadata, forKey: .metadata)
     }
}


That compiles just fine. So the problem is not the dictionary type. (Presumably, if you tried to encode a [String: Any] that contained values of unencodable types, you'd get an exception. Presumably, decoding valid JSON would never fail, since all the value types would be decodable and assignable to Any.)


The problem is that the compiler won't synthesize the above code because of the presence of "Any" as the dictionary value type. This is either a simple bug, an area of Codable support that hasn't been implemented yet, or a more subtle issue. Either way, the correct place to ask about it is over on the swift-users mailing list.

Replies

Are you asking about a synthesized decoder for the Customer type, or are you writing your own code to make Customer conform to Decodable?


Class Dictionary already conforms to Decodable, so the JSON decoder should be able to decode it without any help from you.

I'm asking about how to decode that property since the compiler cannot synthesized the docoding code, it gave me this

`cannot automatically synthesize 'Decodable' because '[String : Any]' does not conform to 'Decodable'
    public let metadata: [String: Any]`

error also I can't think of a way to do it manually too.


I think the Dictionary with a type of its Value be `Any` type doesn't conform to `Decodable` protocol

Since the problem is that a [String: Any] Dictionary can hold values that aren't Decodable, would defining it so that it requires the values be Decodable fix the problem?


let metadata: [String: Decodable]


(I'm not using the Xcode 9 beta yet, so I can't test.)

Nope, it doesn't work.

It works if the value type is a Decodable type, such as [String: String], or even [String: Customer] — assuming you added the Codable conformance to Customer in your actual code.


So I would expect something like [String: Codable] or [String: Any & Codable] to work, but it doesn't. You might need to ask about this over at the swift-users mailing list on swift.org.


This may be a bug, or maybe there needs to be a type-erased type like "AnyCodable", along the lines of AnyHashable, etc. The experts at swift-users should be able to help you.

It looks like you would need to write a custom init(from:) method as described in the "Encode and Decode Manually" section on this page:

https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types


You would get your id and email using the normal CodingKey enum, and then deal with the metadata separately.


Depending on how you are using the metadata json dictionary, you might want to just store the whole decoder.container (or if the metadata is a particular nested dictionary, using nestedContainer(keyedBy:forKey:) the way the sample code gets the additionalInfo container) to access the info later as needed, or you could walk through it using the methods of KeyedDecodingContainer to convert it to whatever form you need.

The problem I found is that there is no method/way that I'm aware of to get an arbitrary dictionary (Dictionary<String, Any>) for the `metadata` property. Also that dictionary can have any keys which I don't know up front, so I can't define their coding key.

If you store the metadata as a KeyedDecodingContainer in your Customer object, you would only need the key to retrieve the metadata nested container (or just store the whole container, if the metadata isn't a particular keyed object). I believe all JSON keys are String type.


You can get a list of the stored keys from the container, but I guess you would also need to provide (or ask for) the object type from wherever you get the arbitrary key you want to look up later. I'm not sure how the "try" decoding methods fail if you request an object that exists but has a different type than you request.

I think you're mixing up two different issues. One is whether the compiler is supposed to synthesize Codable conformance from your Customer class. The other is the correct type for a dictionary of "arbitrary" structure. (Note that because it's coming from JSON, it's not completely arbitrary. All the keys must actually be strings, and all valid JSON values are Codable-conforming.)


So, I tried writing out manually the code that the compiler is supposed to synthesize, which looks like this:


struct Customer: Codable {
     let id: String
     let email: String
     let metadata: [String: Any]
     enum CustomerKeys: String, CodingKey
     {
          case id, email, metadata
     }
     init (from decoder: Decoder) throws {
          let container =  try decoder.container (keyedBy: CustomerKeys.self)
          id = try container.decode (String.self, forKey: .id)
          email = try container.decode (String.self, forKey: .email)
          metadata = try container.decode ([String: Any].self, forKey: .metadata)
     }
     func encode (to encoder: Encoder) throws
     {
          var container = encoder.container (keyedBy: CustomerKeys.self)
          try container.encode (id, forKey: .id)
          try container.encode (email, forKey: .email)
          try container.encode (metadata, forKey: .metadata)
     }
}


That compiles just fine. So the problem is not the dictionary type. (Presumably, if you tried to encode a [String: Any] that contained values of unencodable types, you'd get an exception. Presumably, decoding valid JSON would never fail, since all the value types would be decodable and assignable to Any.)


The problem is that the compiler won't synthesize the above code because of the presence of "Any" as the dictionary value type. This is either a simple bug, an area of Codable support that hasn't been implemented yet, or a more subtle issue. Either way, the correct place to ask about it is over on the swift-users mailing list.

OMG how can I miss that. Thank you. I think it's a Swift compiler bug since I see other issue with Dictionary<String, Any> too. I'll try to report a bug to them.

It seems this issue still not fixed in Xcode 9 final version with swift4.

fatal error: Dictionary<String, Any> does not conform to Decodable because Any does not conform to Decodable.

I think it's not working by design. However I posted this on Stack Overflow and it came with a good workaround here https://stackoverflow.com/a/46049763/885342