Decode url with space in it from decodable model

Viewed 709

I have the following structure and response parsing fails because url (https://example.url.com/test a) has a space in it. How can I escape it with %20 value in deserialization layer and keep it as URL type?

struct Response: Decodable {
    let url: URL

    enum CodingKeys: String, CodingKey {
        case url
    }

    init(from decoder: Decoder) throws {
        self.url = try decoder.container(keyedBy: CodingKeys.self).decode(URL.self, forKey: .url)
    }
}

let string = "{\"url\":\"https://example.url.com/test a\"}"
let responseModel = try? JSONDecoder().decode(Response.self, from: string.data(using: .utf8)!)
print(responseModel?.url)
3 Answers

I don't think that's possible with a JSONDecoder customisation or in the serialization layer as you've mentioned in the post. The best you can achieve would be to do this:

struct Response: Decodable {
    let urlString: String

    var url: URL {
        URL(string: urlString.replacingOccurrences(of: " ", with: "%20"))!
    }

    enum CodingKeys: String, CodingKey {
        case urlString = "url"
    }
}

Note: You don't need init(decoder:) if you don't have a custom implementation. Also don't need the CodingKeys enum if the property names are the same as the string keys (In your case url key is redundant).

As Rob has already mentioned it would be better to fix the issue at the backend instead of fixing the decoding part. If you can not fix it at the server side you can decote your url as a string, percent escape it and then use the resulting string to initialize your url property:

struct Response: Codable {
    let url: URL
}

extension Response {
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let string = try container.decode(String.self, forKey: .url)
        guard
            let percentEncoded = string
                .addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
            let url = URL(string: percentEncoded)
        else {
            throw DecodingError.dataCorruptedError(forKey: .url,
                                                   in: container,
                debugDescription: "Invalid url string: \(string)")
        }
        self.url = url
    }
}

Playground testing

let jsonString = #"{"url":"https://example.url.com/test a"}"#
do {
    let responseModel = try JSONDecoder().decode(Response.self, from: Data(jsonString.utf8))
    print(responseModel.url)
} catch { print(error) }

This will print

https://example.url.com/test%20a

You cannot decode url as an URL, as in decode(URL.self...), because it is not an URL. Change your url property to String, decode this value as String.self, and deal with turning the String into a URL later in the process.

Related