Swift's Decodable gotchas

Even though Swift 4.0 has not been released yet, there is clearly a feature that is going to impact the way we write iOS apps approach JSON (jokes about implementing another library aside!).

I'm not going to recite stdlib docs or original proposal, let's focus on two things that weren't obvious when I decided to actually use Decodable for describing models from "real" API.

Unexpected discovery in auto-conformance to Decodable

Let's imagine we have the following response we want to represent as a struct (ignoring posts object for now):

{
    "count": 1,
    "posts": [
        { "description": "foo" }
    ]
}

Should be easy:

// For some types, conformance is inferred automatically
struct Response: Decodable {  
   let count: Int
}

let decoder = JSONDecoder()

do {  
  let model = try decoder.decode(Response.self, from: data) // Succeeds
} catch {
  ...
}

My usual workflow is to defer describing nested models as much as possible, so next obvious step was to add posts as a raw dictionary:

struct Response: Decodable {  
   let count: Int
   let posts: [String: Any]
}

// Error: Type 'Response' does not conform to protocol 'Decodable'

Hmm, did not work!, however the variant below works:

struct Response: Decodable {  
   let count: Int
   let posts: [String: String]
}

To be honest I was too lazy to investigate the reason (it is almost 2AM), but my assumption is that it has to do with the fact that [String: Any] could contain nested dictionaries/collections as a key which is tricky.

In this case it's actually easy to be slightly less lazy and implement everything needed at once:

struct Response: Decodable {  
   let date: Int
   let posts: [Post]
}

struct Post: Decodable {  
   let description: String
}

and decoding will work as expected.

Decoding arrays

Consider the following JSON structure:

[
  { "description": "foo" },
  { "description": "bar" }
]

Now the top-level object is an array of posts. Since I was refactoring my models I showed previously my first attempt was as below:

struct Response: Decodable {  
   let posts: [Post]
}
...
let model = try decoder.decode(Response.self, from: data)  
...

Decoding fails with error: Expected to decode Dictionary<String, Any> but found an array instead.

It took me some time (well, it's 2AM) to figure that you are able (and, in fact, should) explicitly specify an array as decoding result and remove Response as the top level object does no longer exist in JSON:

let model = try decoder.decode([Post].self, from: data) // it works  
Afterword

After I've finished writing down my findings down they seem to be very obvious. But I hope it could save a few minutes to someone who will be googling the errors and stumbles into this post.