现场级自定义解码器

Field level custom decoder

我正在尝试实现字段级自定义解码,以便可以提供解码器函数来映射值。这最初是为了解决自动将 "Y" 和 "N" 的字符串值转换为 true / false 的问题。有没有更简洁的方法可以做到这一点?

这本打算用于相当大的记录的单个字段...但有点失控了。

主要的 objective 是不必手动实现每个单独的解码 记录中的字段,但要枚举它们并将默认解码器的结果用于没有自定义解码器的任何内容(可能不应该称为 "decoder")。

当前尝试如下所示:

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 [=10=] == "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)
        }
    }
}

使用示例:

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")
    }
}

例如我们有一个例子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)!

如果我们想解析它,我们可以使用 Codable 协议和简单的 NewsCodable 结构:

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

PercentEncodedUrl 是我们为 URL 自定义的 Codable 包装器,它将百分比编码添加到 url 字符串。标准 URL 不支持开箱即用。

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)
    }
}

如果出于某些奇怪的原因我们需要为日期字符串自定义解码器(Date decoding has plenty of support in JSONDecoder),我们可以提供像 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)

所以我们提供了字段级自定义解码器。