しおメモ

雑多な技術系ブログです。ニッチな内容が多いです。

Property wrapperを使ってCodableの一部のプロパティをencodeしないようにする

最近リファクタをしていて、同じオブジェクトを使ってencodeとdecodeをする際に、必要なケースがでてきたので書きました。

状況

以下のHogeのように一部のパラメーターでdecodeはしたいが、なにかの事情でencodeしたくないがためにencode(to:)を実装している状況です。

struct Hoge: Codable {
    var id: Int
    var name: String
    var status: String // これだけencodeしたくない

    enum CodingKeys: String, CodingKey {
        case id
        case name
        case status
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(id, forKey: .id)
        try container.encode(name, forKey: .name)
    }
}

上のようにに書いた場合、パラメーターが増えるたびに、CodingKeysencode(to:)に付け足さなければならなくなるので大変です。

Property wrapperを使った解決

Property wrapperを使ってwrapして、そちらをCodableに準拠させることで楽に解決できます。

@propertyWrapper
struct DecodeOnly<T: Decodable> {
    var wrappedValue: T
}

extension DecodeOnly: Codable {
    init(from decoder: Decoder) throws {
        self.wrappedValue = try T(from: decoder)
    }

    func encode(to encoder: Encoder) throws {}
}

DecodeOnlyというwrapperをCodableにして、encodeの処理は空にしておくことで、decodeのみ行うことができます。(TEncodableである必要はありません。)

ただし、このままだとvalueは空になるのですが、"status": {}のようにkeyは残ってしまうので、KeyedEncodingContainerの方も処理をskipするようにします。

public extension KeyedEncodingContainer {
    func encode<T: Decodable>(_ value: DecodeOnly<T>, forKey key: K) throws {}
}

こうしておけば、上の例のHogeは、

struct Hoge: Codable {
    var id: Int
    var name: String
    @DecodeOnly var status: String
}

のように簡単に書くことが出来ます。

実際にJSONEncoderでencodeしてみると、statusが含まれていないのがわかります。

let hoge = Hoge(id: 12, name: "aaa", status: "yeah")
let encoded = try! JSONEncoder().encode(hoge)
print(String(data: encoded, encoding: .utf8)!) // {"id":12,"name":"aaa"}

Equatableへの応用

似たようなケースで、一部のプロパティだけEquatableで比較したくないといったケースにも対応できます。

struct Fuga: Equatable {
    var id: Int
    var name: String
    var comment: String // これだけ比較したくない

    static func == (lhs: Hoge, rhs: Hoge) -> Bool {
        return lhs.id == rhs.id && lhs.name == rhs.name
    }
}

同じように、property wrapperをEquatableに準拠させて解決します。

@propertyWrapper
struct AlwaysEqual<T> {
    var wrappedValue: T
}

extension AlwaysEqual: Equatable {
    static func == (lhs: AlwaysEqual<T>, rhs: AlwaysEqual<T>) -> Bool {
        return true
    }
}

これを使って、==の実装を省略できます。

struct Fuga: Equatable {
    var id: Int
    var name: String
    @AlwaysEqual var comment: String
}
let fugaA = Fuga(id: 1, name: "aaa", comment: "apple") 
let fugaB = Fuga(id: 1, name: "aaa", comment: "banana")
print(fugaA == fugaB) // true

サンプルコード

こちらにおいてあります👇