Tibrogargan
Tibrogargan

Reputation: 4603

Field level custom decoder

I am trying to implement field level custom decoding, so that a decoder function can be supplied to map a value. This was originally intended to solve automagically turning string values of "Y" and "N" into true / false. Is there a less verbose way I can do this?

This was intended to be used for a single field of a fairly decent sized record... but has gotten somewhat out of hand.

The main objective was not to have to manually implement decoding of every single field in the record, but to enumerate through them and use the result of the default decoder for anything that did not have a custom decoder (which probably should not be called a "decoder").

Current attempt shown below:

class Foo: Decodable {
    var bar: String
    var baz: String

    init(foo: String) {
        self.bar = foo
        self.baz = ""
    }

    enum CodingKeys: String, CodingKey {
        case bar
        case baz
    }

    static func customDecoder(for key: CodingKey) -> ((String) -> Any)? {
        switch key {
        case CodingKeys.baz: return { return $0 == "meow" ? "foo" : "bar" }
        default:
            return nil
        }
    }

    required init(from decoder: Decoder) throws {
        let values: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self)

        if let cde = Foo.customDecoder(for: CodingKeys.bar) {
            self.bar = (try cde(values.decode(String.self, forKey: .bar)) as? String)!
        } else {
            self.bar = try values.decode(type(of: self.bar), forKey: .bar)
        }
        if let cde = Foo.customDecoder(for: CodingKeys.baz) {
            self.baz = (try cde(values.decode(String.self, forKey: .baz)) as? String)!
        } else {
            self.baz = try values.decode(type(of: self.baz), forKey: .baz)
        }
    }
}

Example of use:

func testFoo() {
    var foo: Foo?

    let jsonData = """
        {"bar": "foo", "baz": "meow"}
    """.data(using: .utf8)

    if let data = jsonData {
        foo = try? JSONDecoder().decode(Foo.self, from: data)
        if let bar = foo {
            XCTAssertEqual(bar.bar, "foo")
        } else {
            XCTFail("bar is not foo")
        }
    } else {
        XCTFail("Could not coerce string into JSON")
    }
}

Upvotes: 1

Views: 961

Answers (1)

shio
shio

Reputation: 408

For example we have an example json:

let json = """
{
"id": 1,
"title": "Title",
"thumbnail": "https://www.sample-videos.com/img/Sample-jpg-image-500kb.jpg",
"date": "2014-07-15"
}
""".data(using: .utf8)!

If we want parse that, we can use Codable protocol and simple NewsCodable struct:

public struct NewsCodable: Codable {
    public let id: Int
    public let title: String
    public let thumbnail: PercentEncodedUrl
    public let date: MyDate
}

PercentEncodedUrl is our custom Codable wrapper for URL, which adding percent encoding to url string. Standard URL does not support that out of the box.

public struct PercentEncodedUrl: Codable {
    public let url: URL

    public init(from decoder: Decoder) throws {
        let urlString = try decoder.singleValueContainer().decode(String.self)

        guard
            let encodedUrlString = urlString.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed),
            let url = URL.init(string: encodedUrlString) else {
                throw PercentEncodedUrlError.url(urlString)
        }

        self.url = url
    }

    public enum PercentEncodedUrlError: Error {
        case url(String)
    }
}

If some strange reasons we need custom decoder for date string(Date decoding has plenty of support in JSONDecoder), we can provide wrapper like PercentEncodedUrl.

public struct MyDate: Codable {
    public let date: Date

    public init(from decoder: Decoder) throws {
        let dateString = try decoder.singleValueContainer().decode(String.self)

        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd"
        guard let date = dateFormatter.date(from: dateString) else {
            throw MyDateError.date(dateString)
        }

        self.date = date
    }

    public enum MyDateError: Error {
        case date(String)
    }
}


let decoder = JSONDecoder()
let news = try! decoder.decode(NewsCodable.self, from: json)

So we provided field level custom decoder.

Upvotes: 6

Related Questions