web-dev-qa-db-ja.com

JSONEncoderを使用したプロトコルに準拠したタイプのエンコード/デコード配列

Swiftの新しいJSONDecoder/Encoderを使用して、Swiftプロトコルに準拠する構造体の配列をエンコード/デコードする最適な方法を見つけようとしています。

問題を説明するために小さな例を作成しました。

まず、プロトコルタグと、このプロトコルに準拠するいくつかのタイプがあります。

protocol Tag: Codable {
    var type: String { get }
    var value: String { get }
}

struct AuthorTag: Tag {
    let type = "author"
    let value: String
}

struct GenreTag: Tag {
    let type = "genre"
    let value: String
}

次に、タグの配列を持つタイプ記事があります。

struct Article: Codable {
    let tags: [Tag]
    let title: String
}

最後に、記事をエンコードまたはデコードします

let article = Article(tags: [AuthorTag(value: "Author Tag Value"), GenreTag(value:"Genre Tag Value")], title: "Article Title")


let jsonEncoder = JSONEncoder()
let jsonData = try jsonEncoder.encode(article)
let jsonString = String(data: jsonData, encoding: .utf8)

そして、これは私が欲しいJSON構造です。

{
 "title": "Article Title",
 "tags": [
     {
       "type": "author",
       "value": "Author Tag Value"
     },
     {
       "type": "genre",
       "value": "Genre Tag Value"
     }
 ]
}

問題は、ある時点でtypeプロパティをオンにして配列をデコードする必要がありますが、配列をデコードするにはそのタイプを知る必要があることです。

編集:

Decodableがそのままでは機能しないのに、少なくともEncodableが機能するはずなのは明らかです。次の変更されたArticle構造体はコンパイルされますが、次のエラーメッセージでクラッシュします。

fatal error: Array<Tag> does not conform to Encodable because Tag does not conform to Encodable.: file /Library/Caches/com.Apple.xbs/Sources/swiftlang/swiftlang-900.0.43/src/Swift/stdlib/public/core/Codable.Swift, line 3280

struct Article: Encodable {
    let tags: [Tag]
    let title: String

    enum CodingKeys: String, CodingKey {
        case tags
        case title
    }

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

let article = Article(tags: [AuthorTag(value: "Author Tag"), GenreTag(value:"A Genre Tag")], title: "A Title")

let jsonEncoder = JSONEncoder()
let jsonData = try jsonEncoder.encode(article)
let jsonString = String(data: jsonData, encoding: .utf8)

そして、これはCodeable.Swiftの関連部分です

guard Element.self is Encodable.Type else {
    preconditionFailure("\(type(of: self)) does not conform to Encodable because \(Element.self) does not conform to Encodable.")
}

ソース: https://github.com/Apple/Swift/blob/master/stdlib/public/core/Codable.Swift

32
glektrik

最初の例がコンパイルされない(および2回目がクラッシュする)理由は、 プロトコルがそれ自体に適合しないTagCodableに適合するタイプではないためです。 、したがって、どちらも[Tag]ではありません。そのため、すべてのプロパティがArticleに準拠しているわけではないため、Codableは自動生成Codableに準拠していません。

プロトコルにリストされているプロパティのみをエンコードおよびデコードする

プロトコルにリストされているプロパティをエンコードおよびデコードする場合、1つの解決策は、単にそれらのプロパティを保持し、AnyTag適合性を提供できるCodable型消去を使用することです。

その後、Articleの代わりに、Tagにこの型消去されたラッパーの配列を保持させることができます。

struct AnyTag : Tag, Codable {

    let type: String
    let value: String

    init(_ base: Tag) {
        self.type = base.type
        self.value = base.value
    }
}

struct Article: Codable {
    let tags: [AnyTag]
    let title: String
}

let tags: [Tag] = [
    AuthorTag(value: "Author Tag Value"),
    GenreTag(value:"Genre Tag Value")
]

let article = Article(tags: tags.map(AnyTag.init), title: "Article Title")

let jsonEncoder = JSONEncoder()
jsonEncoder.outputFormatting = .prettyPrinted

let jsonData = try jsonEncoder.encode(article)

if let jsonString = String(data: jsonData, encoding: .utf8) {
    print(jsonString)
}

次のJSON文字列を出力します。

{
  "title" : "Article Title",
  "tags" : [
    {
      "type" : "author",
      "value" : "Author Tag Value"
    },
    {
      "type" : "genre",
      "value" : "Genre Tag Value"
    }
  ]
}

次のようにデコードできます。

let decoded = try JSONDecoder().decode(Article.self, from: jsonData)

print(decoded)

// Article(tags: [
//                 AnyTag(type: "author", value: "Author Tag Value"),
//                 AnyTag(type: "genre", value: "Genre Tag Value")
//               ], title: "Article Title")

適合タイプのすべてのプロパティのエンコードとデコード

ただし、特定のTag準拠タイプのeveryプロパティをエンコードおよびデコードする必要がある場合は、何らかの方法でタイプ情報をJSONに保存することができます。

これを行うには、enumを使用します。

enum TagType : String, Codable {

    // be careful not to rename these – the encoding/decoding relies on the string
    // values of the cases. If you want the decoding to be reliant on case
    // position rather than name, then you can change to enum TagType : Int.
    // (the advantage of the String rawValue is that the JSON is more readable)
    case author, genre

    var metatype: Tag.Type {
        switch self {
        case .author:
            return AuthorTag.self
        case .genre:
            return GenreTag.self
        }
    }
}

コンパイラは、各ケースにメタタイプを提供したことを確認できるため、単純な文字列を使用して型を表すよりも優れています。

次に、タイプを記述するTagプロパティを実装するために適合タイプを必要とするように、staticプロトコルを変更する必要があります。

protocol Tag : Codable {
    static var type: TagType { get }
    var value: String { get }
}

struct AuthorTag : Tag {

    static var type = TagType.author
    let value: String

    var foo: Float
}

struct GenreTag : Tag {

    static var type = TagType.genre
    let value: String

    var baz: String
}

次に、ベースTagTypeとともにTagをエンコードおよびデコードするために、型消去されたラッパーの実装を適応させる必要があります。

struct AnyTag : Codable {

    var base: Tag

    init(_ base: Tag) {
        self.base = base
    }

    private enum CodingKeys : CodingKey {
        case type, base
    }

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

        let type = try container.decode(TagType.self, forKey: .type)
        self.base = try type.metatype.init(from: container.superDecoder(forKey: .base))
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        try container.encode(type(of: base).type, forKey: .type)
        try base.encode(to: container.superEncoder(forKey: .base))
    }
}

指定された適合型のプロパティキーが、型のエンコードに使用されたキーと競合しないようにするために、スーパーエンコーダー/デコーダーを使用しています。たとえば、エンコードされたJSONは次のようになります。

{
  "type" : "author",
  "base" : {
    "value" : "Author Tag Value",
    "foo" : 56.7
  }
}

ただし、競合がないことがわかっていて、プロパティを「=」キーと同じsameレベルでエンコード/デコードして、JSONが次のようになるようにする場合この:

{
  "type" : "author",
  "value" : "Author Tag Value",
  "foo" : 56.7
}

上記のコードでは、container.superDecoder(forKey: .base)の代わりにdecoderを渡すことができ、container.superEncoder(forKey: .base)の代わりにencoderを渡すことができます。

optionalステップとして、Codableの自動生成された適合に依存するのではなく、Articletags実装をカスタマイズできます。 ] _プロパティが[AnyTag]型である場合、エンコード前に[Tag][AnyTag]にボックス化し、デコードのためにボックスを解除する独自の実装を提供できます。

struct Article {

    let tags: [Tag]
    let title: String

    init(tags: [Tag], title: String) {
        self.tags = tags
        self.title = title
    }
}

extension Article : Codable {

    private enum CodingKeys : CodingKey {
        case tags, title
    }

    init(from decoder: Decoder) throws {

        let container = try decoder.container(keyedBy: CodingKeys.self)

        self.tags = try container.decode([AnyTag].self, forKey: .tags).map { $0.base }
        self.title = try container.decode(String.self, forKey: .title)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        try container.encode(tags.map(AnyTag.init), forKey: .tags)
        try container.encode(title, forKey: .title)
    }
}

これにより、tagsプロパティを[Tag]ではなく[AnyTag]型にすることができます。

これで、Tag enumにリストされているTagType準拠タイプをエンコードおよびデコードできます。

let tags: [Tag] = [
    AuthorTag(value: "Author Tag Value", foo: 56.7),
    GenreTag(value:"Genre Tag Value", baz: "hello world")
]

let article = Article(tags: tags, title: "Article Title")

let jsonEncoder = JSONEncoder()
jsonEncoder.outputFormatting = .prettyPrinted

let jsonData = try jsonEncoder.encode(article)

if let jsonString = String(data: jsonData, encoding: .utf8) {
    print(jsonString)
}

JSON文字列を出力します:

{
  "title" : "Article Title",
  "tags" : [
    {
      "type" : "author",
      "base" : {
        "value" : "Author Tag Value",
        "foo" : 56.7
      }
    },
    {
      "type" : "genre",
      "base" : {
        "value" : "Genre Tag Value",
        "baz" : "hello world"
      }
    }
  ]
}

その後、次のようにデコードできます。

let decoded = try JSONDecoder().decode(Article.self, from: jsonData)

print(decoded)

// Article(tags: [
//                 AuthorTag(value: "Author Tag Value", foo: 56.7000008),
//                 GenreTag(value: "Genre Tag Value", baz: "hello world")
//               ],
//         title: "Article Title")
76
Hamish

@Hamishの回答に触発されました。彼のアプローチは理にかなっていると思いましたが、改善されることはほとんどありません。

  1. 配列[Tag][AnyTag]間のマッピングArticleの自動生成Codable適合性なし
  2. static var typeはサブクラスでオーバーライドできないため、基本クラスの配列のコーディング/エンコーディングに同じコードを使用することはできません。 (たとえば、TagAuthorTagおよびGenreTagのスーパークラスである場合)
  3. 最も重要なことは、このコードを別のタイプに再利用できないことです。新しいAny AnotherTypeラッパーを作成する必要があり、それは内部コーディング/エンコードです。

配列の各要素をラップする代わりに、わずかに異なるソリューションを作成し、配列全体にラッパーを作成することができます:

struct MetaArray<M: Meta>: Codable, ExpressibleByArrayLiteral {

    let array: [M.Element]

    init(_ array: [M.Element]) {
        self.array = array
    }

    init(arrayLiteral elements: M.Element...) {
        self.array = elements
    }

    enum CodingKeys: String, CodingKey {
        case metatype
        case object
    }

    init(from decoder: Decoder) throws {
        var container = try decoder.unkeyedContainer()

        var elements: [M.Element] = []
        while !container.isAtEnd {
            let nested = try container.nestedContainer(keyedBy: CodingKeys.self)
            let metatype = try nested.decode(M.self, forKey: .metatype)

            let superDecoder = try nested.superDecoder(forKey: .object)
            let object = try metatype.type.init(from: superDecoder)
            if let element = object as? M.Element {
                elements.append(element)
            }
        }
        array = elements
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.unkeyedContainer()
        try array.forEach { object in
            let metatype = M.metatype(for: object)
            var nested = container.nestedContainer(keyedBy: CodingKeys.self)
            try nested.encode(metatype, forKey: .metatype)
            let superEncoder = nested.superEncoder(forKey: .object)

            let encodable = object as? Encodable
            try encodable?.encode(to: superEncoder)
        }
    }
}

Metaは汎用プロトコルです。

protocol Meta: Codable {
    associatedtype Element

    static func metatype(for element: Element) -> Self
    var type: Decodable.Type { get }
}

これで、タグの保存は次のようになります。

enum TagMetatype: String, Meta {

    typealias Element = Tag

    case author
    case genre

    static func metatype(for element: Tag) -> TagMetatype {
        return element.metatype
    }

    var type: Decodable.Type {
        switch self {
        case .author: return AuthorTag.self
        case .genre: return GenreTag.self
        }
    }
}

struct AuthorTag: Tag {
    var metatype: TagMetatype { return .author } // keep computed to prevent auto-encoding
    let value: String
}

struct GenreTag: Tag {
    var metatype: TagMetatype { return .genre } // keep computed to prevent auto-encoding
    let value: String
}

struct Article: Codable {
    let title: String
    let tags: MetaArray<TagMetatype>
}

結果JSON:

let article = Article(title: "Article Title",
                      tags: [AuthorTag(value: "Author Tag Value"),
                             GenreTag(value:"Genre Tag Value")])

{
  "title" : "Article Title",
  "tags" : [
    {
      "metatype" : "author",
      "object" : {
        "value" : "Author Tag Value"
      }
    },
    {
      "metatype" : "genre",
      "object" : {
        "value" : "Genre Tag Value"
      }
    }
  ]
}

また、JSONをよりきれいに見せたい場合:

{
  "title" : "Article Title",
  "tags" : [
    {
      "author" : {
        "value" : "Author Tag Value"
      }
    },
    {
      "genre" : {
        "value" : "Genre Tag Value"
      }
    }
  ]
}

Metaプロトコルに追加

protocol Meta: Codable {
    associatedtype Element
    static func metatype(for element: Element) -> Self
    var type: Decodable.Type { get }

    init?(rawValue: String)
    var rawValue: String { get }
}

CodingKeysを次のように置き換えます。

struct MetaArray<M: Meta>: Codable, ExpressibleByArrayLiteral {

    let array: [M.Element]

    init(array: [M.Element]) {
        self.array = array
    }

    init(arrayLiteral elements: M.Element...) {
        self.array = elements
    }

    struct ElementKey: CodingKey {
        var stringValue: String
        init?(stringValue: String) {
            self.stringValue = stringValue
        }

        var intValue: Int? { return nil }
        init?(intValue: Int) { return nil }
    }

    init(from decoder: Decoder) throws {
        var container = try decoder.unkeyedContainer()

        var elements: [M.Element] = []
        while !container.isAtEnd {
            let nested = try container.nestedContainer(keyedBy: ElementKey.self)
            guard let key = nested.allKeys.first else { continue }
            let metatype = M(rawValue: key.stringValue)
            let superDecoder = try nested.superDecoder(forKey: key)
            let object = try metatype?.type.init(from: superDecoder)
            if let element = object as? M.Element {
                elements.append(element)
            }
        }
        array = elements
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.unkeyedContainer()
        try array.forEach { object in
            var nested = container.nestedContainer(keyedBy: ElementKey.self)
            let metatype = M.metatype(for: object)
            if let key = ElementKey(stringValue: metatype.rawValue) {
                let superEncoder = nested.superEncoder(forKey: key)
                let encodable = object as? Encodable
                try encodable?.encode(to: superEncoder)
            }
        }
    }
}
4
Vadim Pavlov

受け入れられた答えから引き出された私は、Xcode Playgroundに貼り付けることができる次のコードになりました。このベースを使用して、コード化可能なプロトコルをアプリに追加しました。

出力は次のようになります。without受け入れられた回答に記載されているネスト。

ORIGINAL:
▿ __lldb_expr_33.Parent
  - title: "Parent Struct"
  ▿ items: 2 elements
    ▿ __lldb_expr_33.NumberItem
      - commonProtocolString: "common string from protocol"
      - numberUniqueToThisStruct: 42
    ▿ __lldb_expr_33.StringItem
      - commonProtocolString: "protocol member string"
      - stringUniqueToThisStruct: "a random string"

ENCODED TO JSON:
{
  "title" : "Parent Struct",
  "items" : [
    {
      "type" : "numberItem",
      "numberUniqueToThisStruct" : 42,
      "commonProtocolString" : "common string from protocol"
    },
    {
      "type" : "stringItem",
      "stringUniqueToThisStruct" : "a random string",
      "commonProtocolString" : "protocol member string"
    }
  ]
}

DECODED FROM JSON:
▿ __lldb_expr_33.Parent
  - title: "Parent Struct"
  ▿ items: 2 elements
    ▿ __lldb_expr_33.NumberItem
      - commonProtocolString: "common string from protocol"
      - numberUniqueToThisStruct: 42
    ▿ __lldb_expr_33.StringItem
      - commonProtocolString: "protocol member string"
      - stringUniqueToThisStruct: "a random string"

XcodeプロジェクトまたはPlaygroundに貼り付けて、好みに合わせてカスタマイズします。

import Foundation

struct Parent: Codable {
    let title: String
    let items: [Item]

    init(title: String, items: [Item]) {
        self.title = title
        self.items = items
    }

    enum CodingKeys: String, CodingKey {
        case title
        case items
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        try container.encode(title, forKey: .title)
        try container.encode(items.map({ AnyItem($0) }), forKey: .items)
    }

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

        title = try container.decode(String.self, forKey: .title)
        items = try container.decode([AnyItem].self, forKey: .items).map { $0.item }
    }

}

protocol Item: Codable {
    static var type: ItemType { get }

    var commonProtocolString: String { get }
}

enum ItemType: String, Codable {

    case numberItem
    case stringItem

    var metatype: Item.Type {
        switch self {
        case .numberItem: return NumberItem.self
        case .stringItem: return StringItem.self
        }
    }
}

struct NumberItem: Item {
    static var type = ItemType.numberItem

    let commonProtocolString = "common string from protocol"
    let numberUniqueToThisStruct = 42
}

struct StringItem: Item {
    static var type = ItemType.stringItem

    let commonProtocolString = "protocol member string"
    let stringUniqueToThisStruct = "a random string"
}

struct AnyItem: Codable {

    var item: Item

    init(_ item: Item) {
        self.item = item
    }

    private enum CodingKeys : CodingKey {
        case type
        case item
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        try container.encode(type(of: item).type, forKey: .type)
        try item.encode(to: encoder)
    }

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

        let type = try container.decode(ItemType.self, forKey: .type)
        self.item = try type.metatype.init(from: decoder)
    }

}

func testCodableProtocol() {
    var items = [Item]()
    items.append(NumberItem())
    items.append(StringItem())
    let parent = Parent(title: "Parent Struct", items: items)

    print("ORIGINAL:")
    dump(parent)
    print("")

    let jsonEncoder = JSONEncoder()
    jsonEncoder.outputFormatting = .prettyPrinted
    let jsonData = try! jsonEncoder.encode(parent)
    let jsonString = String(data: jsonData, encoding: .utf8)!
    print("ENCODED TO JSON:")
    print(jsonString)
    print("")

    let jsonDecoder = JSONDecoder()
    let decoded = try! jsonDecoder.decode(type(of: parent), from: jsonData)
    print("DECODED FROM JSON:")
    dump(decoded)
    print("")
}
testCodableProtocol()
2
pkamb