Introduction
Add an enum JSONDecoder.URLDecodingStrategy in JSONDecoder to customize decoding URL.
Motivation
Currently JSONDecoder in Foundation framework implements URL decoding like below
} else if type == URL.self {
guard let urlString = try self.unbox(value, as: String.self) else {
return nil
}
guard let url = URL(string: urlString) else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath,
debugDescription: "Invalid URL string."))
}
return url
}
JSONDecoder decodes URL from string.
Sometimes, We need preprocessing to init URL.
There is some example
/// case1 : When we need trimming white spaces before initialize URL
let trimmedString = stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
let url1 = URL.init(string: trimmedString)
/// case2 : When we need combine domainURL and path
let domainURLString = "https://forums.swift.org"
let pitchPath = "/c/evolution/pitches"
let url2 = URL.init(string: domainURLString + pitchPath)
Unfortunatly, Current URL Decoding implementation can't decode that cases.
Proposed solution
So, I suggest to JSONDecoder to add enum JSONDecoder.URLDecodingStrategy to JSONDecoder
Fortunatly, func decode<T>(T.Type, from data: Data) -> T
method converts json data to json object.
// MARK: - Decoding Values
/// Decodes a top-level value of the given type from the given JSON representation.
///
/// - parameter type: The type of the value to decode.
/// - parameter data: The data to decode from.
/// - returns: A value of the requested type.
/// - throws: `DecodingError.dataCorrupted` if values requested from the payload are corrupted, or if the given data is not valid JSON.
/// - throws: An error if any value throws an error during decoding.
open func decode<T : Decodable>(_ type: T.Type, from data: Data) throws -> T {
let topLevel: Any
do {
topLevel = try JSONSerialization.jsonObject(with: data, options: .allowFragments)
} catch {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: error))
}
let decoder = _JSONDecoder(referencing: topLevel, options: self.options)
guard let value = try decoder.unbox(topLevel, as: type) else {
throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: [], debugDescription: "The given data did not contain a top-level value."))
}
return value
}
Detailed design
class JSONDecoder {
enum URLDecodingStrategy {
case fromString // decode URL from string
case custom((Decoder) throws -> URL)
}
var urlDecodingStrategy: URLDecodingStrategy = .fromString // default value
}
let jsonDecoder = JSONDecoder()
jsonDecoder.urlDecodingStrategy = .custom { decoder -> URL in
guard let codingPath = decoder.codingPath.last else {
throw DecodingError.dataCorrupted(DecodingError.Context.init(codingPath: decoder.codingPath, debugDescription: "coding key is not exist"))
}
// trimming whitespaces and new lines before initialize URL
let urlString = try decoder.singleValueContainer().decode(String.self).trimmingCharacters(in: .whitespacesAndNewlines)
if let url = URL.init(string: urlString) {
return url
} else {
throw SomeError
}
}
If JSONDecoder provides URLDecodingStrategy, we can decode above two cases and more cases.
let jsonDecoder = JSONDecoder()
jsonDecoder.urlDecodingStrategy = .custom {
}
Source compatibility
It is not affect to previous swift version.