Sounds simple, right? But yesterday I messed up this simple operation and was stuck for a couple hours wondering why an API request wasn’t working.
Background
A pattern I follow is to use Swift struct
‘s to represent each network request my app needs. Each struct has only let
properties and conforms to Encodable
so that it can be serialized to JSON and sent to the server by my networking class. Typically this is straightforward and the top-level JSON object is almost always a dictionary.
For example:
struct MyRequest: Encodable { let uid: String let date: Date let bar: Int? }
Thanks to the magic of the compiler, this will encode to a JSON dictionary just fine without any boiler plate code on my side.
Occasionally if the names of the properties don’t match the names of the parameters the API expects (typically because the API is not in my control and follows some naming convention I don’t like – or even no naming convention whatsoever), then I need to declare CodingKeys
to properly map the encoding.
Some times there’s no avoiding it though and I need to implement the encode
function myself. I often need this when the server expects some weird Date format or some business logic is needed for the encoding.
The Issue
Yesterday I implemented a simple network request that needed to be serialized as an array.
struct CallsRequest: Encodable { let calls: [Call] // Call conforms to Encodable }
The trouble is that this would encode to a Dictionary with one key “calls” whose value is the array. What I needed was to encode to an Array instead.
func encode(to encoder: Encoder) throws { var values = encoder.unkeyedContainer() try values.encode(calls) }
I had never used unkeyedContainer()
before. Previously I’ve always used container(keyedBy:)
, which encodes as a dictionary (hence keyed).
The output of the approach above was an object of type [[Call]]
. That is, it encodes an array of arrays instead of just an array. (Someone else spotted the extra brackets in the JSON output. Thanks Anton!)
It took me a while to realize where the extra level of Array was coming from, but then it dawned on me: the unkeyedContainer()
is an Array, just as the container(keyedBy:)
is a Dictionary.
So what I really needed was something like this:
func encode(to encoder: Encoder) throws { var values = encoder.unkeyedContainer() for call in calls { try values.encode(call) } }
This delivered the expected output of type [Call]
. Problem solved.
Anyway, I hope that someday this might save someone a few hours of frustration.
Update
Where the real JSON encoding happens is via another custom protocol that I didn’t mention. Basically I want objects to be able to convert themselves to Data
so that they could be set as the body to an HTTP PUT/POST/PATCH/DELETE request.
public protocol ParametersBuilder { func data() throws -> Data func dictionary() throws -> [String: Any] }
dictionary()
is for GET requests or for .formUrlEncoded
request bodies. But data()
is used for everything else.
The default implementation of data()
is this:
extension Encodable { public func data() throws -> Data { return try JSONEncoder().encode(self) } }
(which is why CallsRequest conforms to Encodable
and what by default would turn it into a dictionary)
A simpler solution to my problem would have been to provide a custom implementation of data()
on CallsRequest instead of conforming to Encodable
:
struct CallsRequest: ParametersBuilder { let calls: [Call] // Call conforms to Encodable public func data() throws -> Data { return try JSONEncoder().encode(calls) } }
Update 2
Rob Napier pointed out to me that I could use singleValueContainer
to skip having to loop through the array.
func encode(to encoder: Encoder) throws { var values = encoder.singleValueContainer() try values.encode(calls) }