🦔

swiftでprotocolをCodableにした時に出るエラーの対処方法

2022/02/19に公開

protocolCodableにしてそれを継承したstructを他のstructのプロパティにしたらCodableでエラーが出た。

問題のコード

まずはこんなprotocolを作成。

Shapes.swift
protocol ShapeProtocol: Codable {
	var position: PVector {get}
	func hasIntersection(ray: Ray) -> Intersection?
}

PVectorCodablestruct

コード
PVector.swift
struct PVector: Codable {
    let x: Double
    let y: Double
    let z: Double
}

このShapeProtocolに対応したSphereのコードが以下。

Shapes.swift
struct Sphere: ShapeProtocol {
    let radius: Double

    // ShapeProtocol
    var position: PVector
    
    init(position: PVector, radius: Double) {
        self.radius = radius
        self.position = position
    }
    
    // ShapeProtocol
    func hasIntersection(ray: Ray) -> Intersection? {
        return nil
    }
}

この状態でSphereのインスタンスを作成してエンコード&デコードしてみる。

CodableSampleTests.swift
        let sphere = Sphere(position: PVector(0,0,0), radius: 2.0)
        let encoder = JSONEncoder()
        encoder.outputFormatting = .prettyPrinted
        let encodedSphereData = try encoder.encode(sphere)
        guard let sphereJson = String(data: encodedSphereData, encoding: .utf8) else {
            XCTAssert(true)
            return
        }
        print("encoded: \(sphereJson)")

        let decoder = JSONDecoder()
        guard let sphereData = sphereJson.data(using: .utf8) else {
            XCTAssert(true)
            return
        }
        let decodedSphere = try decoder.decode(Sphere.self, from: sphereData)
        print("decoded: \(decodedSphere)")

結果は下記。

encoded: {
  "position" : {
    "x" : 0,
    "y" : 0,
    "z" : 0
  },
  "radius" : 2
}
decoded: Sphere(radius: 2.0, position: CodableSample.PVector(x: 0.0, y: 0.0, z: 0.0))

ちゃんとできてる。

次に[ShapeProtocol]をプロパティに持つSceneを作成。

Scene.swift
struct Scene: Codable {
    let position: PVector
    let shapes: [ShapeProtocol]
}

するとlet shapes: [ShapeProtocol]で下記のエラーが出る。

Type 'Scene' does not conform to protocol 'Decodable'
Type 'Scene' does not conform to protocol 'Encodable'

念のためにこの行をコメントアウトしてlet position: PVectorだけにするとエラーは出ない。

念のためにSceneCodableなコードを足してみる。

Scene.swift
struct Scene: Codable {
    let position: PVector
    let shapes: [ShapeProtocol]
    
    // Codable
    enum CodingKeys: String, CodingKey {
        case position, shapes
    }
    
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        self.position = try values.decode(PVector.self, forKey: .position)
        self.shapes = try values.decode([ShapeProtocol].self, forKey: .shapes)
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(position, forKey: .position)
        try container.encode(shapes, forKey: .shapes)
    }
}

するとshapesをデコード・エンコードする箇所で以下のエラーが出る。

Protocol 'ShapeProtocol' as a type cannot conform to 'Decodable'
Protocol 'ShapeProtocol' as a type cannot conform to 'Encodable'

ShapeProtocolCodableなのになぜ?

Protocol doesn't confirm to itself

ググってみると出てきたのがstackoverflowのこのページ。色々なページから辿ると結局このページにやってくる。
https://stackoverflow.com/questions/33112559/protocol-doesnt-conform-to-itself
今回のエラーで考えるとまずprotocolは継承するべきものの定義であって、protocolそのものが継承しているprotocolに対応しているわけではないということか。ShapeProtocolは自身を継承したstructSphereCodableを強いるけど、ShapeProtocolそのものはCodableに対応しているわけではない。だから[ShapeProtocol]な型のshapesCodableでないのでSceneCodableに対応(confirm)できていないとエラーが出る。

で、どうすれば良いのか?

Codableなラッパーで包む

このページに2つの解決方法が載っていた。
https://stackoverflow.com/questions/50205373/make-a-protocol-codable-and-store-it-in-an-array
ラッパーを使うかenumを使うか。
今回は特に考えなしに最初の解決策だったラッパーで包んでみた。

Sceneを以下のように変更。

Scene.swift
struct Scene: Codable {
    let position: PVector
    let shapes: [ShapeProtocol]
    
    enum CodingKeys: String, CodingKey {
        case shapes, position
    }
    
    init(position: PVector, shapes: [ShapeProtocol]) {
        self.position = position
        self.shapes = shapes
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.position = try container.decode(PVector.self, forKey: .position)
        let wrappers = try container.decode([ShapeWrapper].self, forKey: .shapes)
        self.shapes = wrappers.map { $0.shape }
    }
    
    func encode(to encoder: Encoder) throws {
        let wrappers = shapes.map { ShapeWrapper($0) }
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(position, forKey: .position)
        try container.encode(wrappers, forKey: .shapes)
    }
}

fileprivate struct ShapeWrapper: Codable {
    let shape: ShapeProtocol
    
    enum CodingKeys: String, CodingKey {
        case base, payload
    }
    
    enum Base: String, Codable {
        case sphere
    }
    
    init(_ shape: ShapeProtocol) {
        self.shape = shape
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let base = try container.decode(Base.self, forKey: .base)
        
        switch base {
        case .sphere:
            self.shape = try container.decode(Sphere.self, forKey: .payload)
        }
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        
        switch shape {
        case let payload as Sphere:
            try container.encode(Base.sphere, forKey: .base)
            try container.encode(payload, forKey: .payload)
        default:
            break
        }
    }
}

基本的に載っていた方法そのままだがjson文字列を見る時に数字でなく名前で見られるようにBaseIntからStringに変更してある。
やっていることはエラーになっていた箇所でShapeProtocolでなくCodablestructShapeWrapperをデコード・エンコードする様になっている。これでエラーは出なくなった。

デコード・エンコードの結果
scene encoded: {
  "shapes" : [
    {
      "base" : "sphere",
      "payload" : {
        "position" : {
          "x" : 0,
          "y" : 0,
          "z" : 0
        },
        "radius" : 2
      }
    },
    {
      "base" : "sphere",
      "payload" : {
        "position" : {
          "x" : 10,
          "y" : 20,
          "z" : 30
        },
        "radius" : 5
      }
    }
  ],
  "position" : {
    "x" : 1,
    "y" : 2,
    "z" : 3
  }
}
scene decoded: Scene(position: CodableSample.PVector(x: 1.0, y: 2.0, z: 3.0), shapes: [CodableSample.Sphere(radius: 2.0, position: CodableSample.PVector(x: 0.0, y: 0.0, z: 0.0)), CodableSample.Sphere(radius: 5.0, position: CodableSample.PVector(x: 10.0, y: 20.0, z: 30.0))])

めでたし、めでたし。

動くコード

最終的なコードはこちら。
https://github.com/paraches/CodableSample

最後に

今回の解決方法だとShapeProtocolを継承するstructが増える度にShapeWrapperにも書き加える必要が出てくる。これはenumを使った方法でも同じ。面倒なことになるだけで本末転倒な気がする…。

Discussion