Search code examples
swifturlsessionjson-serializationurlrequest

Problem encoding JSON for a UrlRequest in Swift


I'm trying to set up an API service. I have a simple login call that looks like this:

enum HttpMethod: Equatable {
    case get([URLQueryItem])
    case put(Data?)
    case post(Data?)

    var name: String {
        switch self {
        case .get: return "GET"
        case .put: return "PUT"
        case .post: return "POST"
        }
    }
}

struct Request<Response> {
    let url: URL
    let method: HttpMethod
    var headers: [String: String] = [:]
}

extension Request {
    static func postUserLogin(email: String, password:String) -> Self {
        var params = [String: Any]()
        params["username"] = email
        params["password"] = password
        let data = params.jsonData

        return Request(
            url: URL(string: NetworkingConstants.USER_LOGIN)!,
            method: .post(
                try? JSONEncoder().encode(data) //should this be something else???
            )
        )
    }
}

extension Dictionary {
    var jsonData: Data? {
        return try? JSONSerialization.data(withJSONObject: self, options: [.prettyPrinted])
    }
}

extension Request {
    var urlRequest: URLRequest {
        var request = URLRequest(url: url)

        switch method {
        case .post(let data), .put(let data):
            request.httpBody = data

        case let .get(queryItems):
            //removed for brevity
        default:
            break
        }

        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.allHTTPHeaderFields = headers
        request.httpMethod = method.name
        return request
    }
}

extension URLSession {
    func decode<Value: Decodable>(
        _ request: Request<Value>,
        using decoder: JSONDecoder = .init()
    ) async throws -> Value {
        let decoded = Task.detached(priority: .userInitiated) {
            let (data, _) = try await self.data(for: request.urlRequest)
            try Task.checkCancellation()
            return try decoder.decode(Value.self, from: data)
        }
        return try await decoded.value
    }
}

The problem is, instead of the Body of the request being sent to the server looking like this:

{
    "username": "[email protected]",
    "password": "abcdef"
}

It looks like this:

"eyJwYXNzd29yZCI6ImFhYWEiLCJ1cZSI6InRhQHQuY29tIn0="

What am I doing wrong? Here's how I make the call:

let request: Request<UserLoginResponse> = .postUserLogin(email: "[email protected]", password: "abcdef")
let response = try? await URLSession.shared.decode(request)

request.httpBody looks like this:

▿ Optional<Data>
  ▿ some : 74 bytes
    - count : 74
    ▿ pointer : 0x00007fcc74011200
      - pointerValue : 140516096283136

Solution

  • JSONEncoder and JSONDecoder are designed to work directly with Swift types allowing you to skip the need to build generic dictionaries or arrays as you are doing where you have this code:

    // You should use a Swift type instead of this
    var params = [String: Any]()
    params["username"] = email
    params["password"] = password
    let data = params.jsonData
    

    In your case you should declare a struct and mark it as Codable then encode that (from a playground):

    struct UserAuthentication: Codable {
        let username: String
        let password: String
    }
    
    let email = "fred"
    let password = "foobar"
    
    let userAuthentication = UserAuthentication(username: email, password: password)
    let encoder = JSONEncoder()
    
    do {
        let encoded = try encoder.encode(userAuthentication)
        print(String(data: encoded, encoding: .utf8)!)
    } catch (let error) {
        print("Json Encoding Error \(error)")
    }
    

    Swift will represent your struct (in this case the UserAuthentication struct) as JSON using the names of the properties as the JSON keys and their values as the JSON values.

    If you want to handle more complex cases, see:

    Encoding and Decoding Custom Types