acecilia
(Andrés Cecilia Luque)
1
Hello everybody 
The following code builds and runs, and prints ["key": "value"], as expected:
typealias Dict = [String: String]
do {
let decoder = JSONDecoder()
let string = Data("""
{
"key": "value"
}
""".utf8)
let decoded = try decoder.decode(Dict.self, from: string)
print(decoded)
}
When I change the type of the key as follows:
struct String2: Decodable, Hashable {
let value: String
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
value = try container.decode(String.self)
}
}
typealias Dict = [String2: String]
do {
let decoder = JSONDecoder()
let string = Data("""
{
"key": "value"
}
""".utf8)
let decoded = try decoder.decode(Dict.self, from: string)
print(decoded)
}
I would expect it to behave similarly. The reality is that the code builds, but when running shows the following error:
Fatal error: Error raised at top level: Swift.DecodingError.typeMismatch(Swift.Array<Any>, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Array<Any> but found a dictionary instead.", underlyingError: nil)): file /AppleInternal/BuildRoot/Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-1103.8.25.8/swift/stdlib/public/core/ErrorType.swift, line 200
Such code does not even reach the content of the init(from decoder: Decoder) throws function inside the String2 type.
Am I missing something? Any idea why this happens?
Thanks,
Andres
Lantua
2
Dictionary has the following coding strategy:
-
If Key is String or Int and nothing else, it uses keyed container which, for JSON, corresponds to
{ "key1": <value1>, "key2": <value2>, ... }
-
Otherwise, it uses unkeyed container and encode it like this:
[ <key1>, <value1>, <key2>, <value2>, ... ]
The error is raise because to data is an object, but you're expecting an array (because of rule 2), which it detects long before init(from:).
1 Like
acecilia
(Andrés Cecilia Luque)
3
I see. Thanks for the answer.
Is it unreasonable to expect the second piece of code to work as I mentioned?
Lantua
4
It was discussed before a few times, here for one.
1 Like
See also the discussion in Bug or PEBKAC? (and my response in the thread for some background on why this is the case).
1 Like
Lantua
6
Should be this one (I tried to link there, but couldn't find the post
).
(Thanks, fixed! Copy-pasta...
)
1 Like
rnmp
(Rolando Murillo)
8
Ran into this same issue. I wanted a Dictionary instead of a struct because order doesn't matter. Thus I went with this solution which is a bit clunky but does the job.
enum ToolbarMark: String {
case bold
case italic
case strikethrough
case ul
case ol
case h1
case h2
case h3
case p
case action_item
}
// ...
if let legitString = messageBody["toolbarState"]?.data(using: .utf8) {
do {
// Swift does not support parsing of JSON into dictionaries where keys
// are not strings or integers. Thus we first parse as strings then cast
// appropriately. We are not using structs for toolbar state because
// order doesn't matter and that's not something we want to enforce yet
let jsonObject = try JSONSerialization.jsonObject(with: legitString) as? [String: Bool]
var nextToolbarState: [ToolbarMark:Bool] = [:]
jsonObject?.forEach({ (key: String, value: Bool) in
if let mark = ToolbarMark.init(rawValue: key) {
nextToolbarState[mark] = value
}
})
textEditor.setToolbarState(nextToolbarState)
} catch (let err) {
print(err)
}
}
I come from a JS background so I may be missing much of the context of why I might be wrong but thought I'd share either way.
itaiferber
(Itai Ferber)
9
As of SE-0320: Allow coding of non String / Int keyed Dictionary into a KeyedContainer and Swift 5.6, you should now be able to conform your ToolbarMark enum to CodingKeyRepresentable (benefitting from the default implementation that String- and Int-backed RawRepresentable types get) and be able to decode [ToolbarMark: Bool] directly:
enum ToolbarMark: String, Decodable, CodingKeyRepresentable {
case bold
case italic
case strikethrough
case ul
case ol
case h1
case h2
case h3
case p
case action_item
}
let data = """
{ "bold": true,
"italic": false,
"action_item": true }
""".data(using: .utf8)!
let marks = try JSONDecoder().decode([ToolbarMark: Bool].self, from: data)
print(marks)
// => [ToolbarMark.action_item: true, ToolbarMark.bold: true, ToolbarMark.italic: false]
This also works with assigning explicit raw values to your enum cases, so instead of having
enum ToolbarMark: ... {
...
case action_item
}
you can write
enum ToolbarMark: ... {
...
case actionItem = "action_item"
}
to match Swift naming conventions.
4 Likes
rnmp
(Rolando Murillo)
10
Thank you. Appreciate you taking the time write an update and suggestions!
1 Like