Usage of emptyResponseCodes or perhaps a custom response serializer?

I'm dealing a JSON service that returns 200 as the response code and "null" as the response body to indicate when the requested item couldn't be found.

I've read up on the usage of emptyResponseCodes: [200, ...], but it doesn't seem to address this particular scenario. What would be the recommended way for dealing with this if I still want to use responseDecodable()?

I've tried the following, but the failure case occurs:

self.sessionManager.request(self.url, headers: self.headers).validate().responseDecodable(of: T.self, queue: self.queue, emptyResponseCodes: [200, 204]) { response in
    switch response.result {
    case .success(let data):
    case .failure(let error):

Would a custom response serializer be the best approach? Something like this?

self.sessionManager.request(self.url, headers: self.headers).response(responseSerializer: NullResponseSerializer<T>()) { response in

Where NullResponseSerializer would be something like:

struct NullResponseSerializer<T: Decodable>: ResponseSerializer {
    func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> Result<T, Error> {
        guard let response = response else { return .failure(error!) }
        do {
            if response.statusCode >= 200 || response.statusCode <= 300 {
                let nullData = Data(bytes: [0x6E, 0x75, 0x6C, 0x6C] as [UInt8], count: 4)
                if let d = data, d == nullData {
                    print("need to deal with null data...")
                    // TODO: Deal with this case.
                } else {
                    let result = try serialize(request: request, response: response, data: data, error: nil)
                    return .success(result as! T)
            } else {
                return .failure(error)
        } catch {
            return .failure(error)


So it literally just replies with null (or "null") in the failure case? In that case, yes, a custom serializer is your best bet. In your case I would perform the initial response checking in your serializer and then pass it off to a DecodableResponseSerializer instance. Also, response serializers no longer return Result values in Alamofire 5.

struct NullResponseSerializer<T: Decodable>: ResponseSerializer {
    func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> T {
        guard let response = response else { throw error! }

        if response.statusCode, data == Data("null".utf8) // or Data(#""null""#.utf8) {
            throw AFError.responseSerializationFailed(reason: .customSerializationFailed(error: YourCustomError))
        return try DecodableResponseSerializer<T>(...).serialize(request: request, response: response, data: data, error: error)

Technically you could instead add a DataPreprocessor to the existing DecodableResponseSerializer if you wanted to rewrite the null body into a particular value before serialization. That might be a good idea if there's an existing common payload you can replicate.

Thanks, this is great feedback!

I know that the server will respond to multiple requests in this manner (200 and "null") for both GET and POST requests in the event that nothing is found.

In your opinion, what is the benefit of adding a DataPreprocessor instead of using a custom ResponseSerializer? I haven't tried the former yet and I'm wondering if it offers some benefit in conjunction with responseDecodable.

In this case, would it ease parsing of the body such that if the body were nil, a different error would be thrown?


Biggest advantage would be the ability to reuse an exist response type to represent this failure condition rather than throwing the error. But if you don't have that then it really doesn't help much.

If no body data is returned for a 200 an error will be produced by the built in serializers unless you add 200 to the set of acceptable empty response codes.

Terms of Service

Privacy Policy

Cookie Policy