Because of [AnyHashable: Any]
(I need it) I have to implement init(from:)
and encode(to:)
. But when I run it it fails to decode the property with array value:
typeMismatch(Swift.String, Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "items", intValue: nil)], debugDescription: "Expected to decode String but found an array instead.", underlyingError: nil))
This is the code you can run in Playground:
struct ServerResponse: Codable {
var headers: [AnyHashable: Any]?
var items: [Item]?
enum CodingKeys: String, CodingKey {
case items, headers
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: ServerResponse.CodingKeys.self)
items = try container.decode([Item].self, forKey: ServerResponse.CodingKeys.items)
let singleValueContainer = try decoder.singleValueContainer()
let stringDictionary = try singleValueContainer.decode([String: String].self)
headers = [:]
for (key, value) in stringDictionary {
headers?[key] = value
}
}
public func encode(to encoder: Encoder) throws {
let stringDictionary: [String: String] = Dictionary(
uniqueKeysWithValues: headers?.map {("\($0)", "\($1)")} ?? []
)
var singleValueContainer = encoder.singleValueContainer()
try singleValueContainer.encode(stringDictionary)
var container = encoder.container(keyedBy: ServerResponse.CodingKeys.self)
try container.encode(items, forKey: ServerResponse.CodingKeys.items)
}
struct Item: Codable {
let name: String
}
}
let testData = """
{
"items": [
{"name": "John"},
{"name": "Duo"}
]
}
""".data(using: .utf8)!
let decoder = JSONDecoder()
do {
let response = try decoder.decode(ServerResponse.self, from: testData)
print(response)
} catch {
print(error)
}
What is wrong with it? Why does it complain about getting String
while I have put an array?
If I remove headers from the struct and conform to Codable
everything works fine.
The issue here lies in how you are attempting to extract items from this top-level dictionary, by attempting to decode it as a dictionary. Specifically,
let singleValueContainer = try decoder.singleValueContainer()
let stringDictionary = try singleValueContainer.decode([String: String].self)
is the problematic snippet. With your particular JSON payload, singleValueContainer
here wraps up
{
"items": [ ... ],
"..." // <- assuming there are other actual keys and values
}
This is valid to do, but when you attempt to decode the contents of the container as [String: String]
, you are asserting that you expect the container to contain specifically a dictionary with String
keys and String
values; the value for the items
key, however, is not a string, but an array.
When you have a collection with arbitrary values, the correct approach to extracting its contents is to use a keyed container instead. Specifically, you can use a keyed container whose key type can take on any String
or Int
value, as such:
struct AnyCodingKey: CodingKey {
let intValue: Int?
let stringValue: String
init?(intValue: Int) {
self.intValue = intValue
self.stringValue = "\(intValue)"
}
init?(stringValue: String) {
intValue = Int(stringValue)
self.stringValue = stringValue
}
}
With this coding key type, you can then request decoder
as another keyed container — and this time, the keys can be arbitrary:
let untypedContainer = try decoder.container(keyedBy: AnyCodingKey.self)
The trick is that now, the values in untypedContainer
are not asserted to be of any type until you try to decode them. You can then iterate over untypedContainer.allKeys
(same as you iterate over stringDictionary
at the moment), and for each key, you can decide how you want to decode(_:forKey:)
out of the container. You could:
String
, and if you get a DecodinerError.typeMismatch
error, just skip the key-value pairFor example:
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: ServerResponse.CodingKeys.self)
items = try container.decode([Item].self, forKey: ServerResponse.CodingKeys.items)
headers = [:]
let untypedContainer = try decoder.container(keyedBy: AnyCodingKey.self)
for key in untypedContainer.allKeys {
do {
let value = try untypedContainer.decode(String.self, forKey: key)
headers[key.stringValue] = value
} catch DecodingError.typeMismatch { /* skip this key-value pair */ }
}
}