Search code examples
jsonswifturlcodabledecodable

Decode URLs with special characters in Swift


An API I work with provides URL links, that can contain special characters like "http://es.dbpedia.org/resource/Análisis_de_datos" (the letter "á" inside).

It's an absolutely valid URL, however, if a decodable class contains an optional URL? variable, it can't be decoded.

I can change URL? to String? in my class and have a computed variable with something like URL(string: urlString.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed) but perhaps there is a more elegant solution.

To reproduce in Playground:

struct Container: Encodable {
    let url: String
}

struct Response: Decodable {
    let url: URL?
}

let container = Container(url: "http://es.dbpedia.org/resource/Análisis_de_datos")

let encoder = JSONEncoder()
let encodedData = try encoder.encode(container)
    
let decoder = JSONDecoder()
let response = try? decoder.decode(Response.self, from: encodedData)
// response == nil, as it can't be decoded.

let url = response?.url 

Solution

  • There are multiple way to overcome this, but I think using a property wrapper is probably the most elegant:

    @propertyWrapper
    struct URLPercentEncoding {
       var wrappedValue: URL
    }
    
    extension URLPercentEncoding: Decodable {
       public init(from decoder: Decoder) throws {
          let container = try decoder.singleValueContainer()
            
          if let str = try? container.decode(String.self),
             let encoded = str.addingPercentEncoding(
                                  withAllowedCharacters: .urlFragmentAllowed),
             let url = URL(string: encoded) {
    
             self.wrappedValue = url
    
          } else {
             throw DecodingError.dataCorrupted(
                .init(codingPath: container.codingPath, debugDescription: "Corrupted url"))
          }
       }
    }
    

    Then you could use it like so without the consumer of this model having to know anything about it:

    struct Response: Decodable {
        @URLPercentEncoding let url: URL
    }