Decoding invalid URLs as nil

Viewed 1352

Before you answer:

I'm aware:

  • That an empty string is an invalid URL
  • That I could write a custom decoder for Employee
  • That I could declare url as a String

What I'm looking for is a better solution for decoding the optional URL itself. I'm hoping there's some Codable magic I'm missing!


So, I have JSON such as

let json = Data("""
                {
                    "name": "Fred",
                    "url": ""
                }
                """.utf8)

and a corresponding object that contains an optional URL…

struct Employee: Decodable {
    let name: String
    let url: URL?
}

As url in the JSON is invalid, I'd like it to decode as nil, rather than throwing an error.

Trying the following doesn't work (it doesn't get called)…

extension Optional where Wrapped == URL {
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        do {
            self = try container.decode(URL.self)
        } catch {
            self = nil
        }
    }
}

In the past I've used…

struct FailableDecodable<T: Decodable>: Deodable {

    let wrapped: T?

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        do {
            self.wrapped = try container.decode(T.self)
        } catch {
            print("Error decoding failable object: \(error)")
            self.wrapped = nil
        }
    }
}

struct Employee: Decodable {
    let name: String
    let url: FailableDecodable<URL>?
}

but this requires me to continually refer to url.wrapped.

Is there a better solution?

2 Answers

If you are using Swift 5.1, you can use @propertyWrapper:

let json = """
{
    "name": "Fred",
    "url": ""
}
""".data(using: .utf8)!


@propertyWrapper
struct FailableDecodable<Wrapped: Decodable>: Decodable {
    var wrappedValue: Wrapped?

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        wrappedValue = try? container.decode(Wrapped.self)
    }
}

struct Employee: Decodable {
    let name: String

    @FailableDecodable
    private(set) var url: URL?
}

let employee = try! JSONDecoder().decode(Employee.self, from: json)
employee.url // nil

Edit — Codable version

If you need the top level struct to be Encodable as well you can use Codable conformance to the property wrapper.

@propertyWrapper
struct FailableDecodable<Wrapped: Codable>: Codable {
    var wrappedValue: Wrapped?

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        wrappedValue = try? container.decode(Wrapped.self)
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(wrappedValue)
    }
}

If url is nil this will output a JSON with url: null

{"name":"Fred","url":null}

If you wish to don't output the url property when nil you will need to implement a custom encoding (with encode(to:)) in Employee (which would mitigate the benefit of using a property wrapper).

Note: Using the default implementation of encode(to:) (not implementing it) works, but output an empty object when url is nil:

{"name":"Fred","url":{}}

If you get Return from initializer without initializing all stored properties warning for your Codable struct initializer that contains @FailableDecodable below will resolve it.

@propertyWrapper
struct FailableDecodable<Wrapped: Codable>: Codable {
    
    var wrappedValue: Wrapped?
    
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        wrappedValue = try? container.decode(Wrapped.self)
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(wrappedValue)
    }
    
    init() {
        self.wrappedValue = nil
    }
    
}
Related