Кодирование/декодирование массивов типов, соответствующих протоколу с JSONEncoder

Я пытаюсь найти лучший способ кодировать/декодировать массив структур, соответствующих быстрому протоколу, используя новый JSONDecoder/Encoder в Swift 4.

Я составил небольшой пример, чтобы проиллюстрировать проблему:

Сначала у нас есть тег протокола и несколько типов, которые соответствуют этому протоколу.

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

Проблема в том, что в какой-то момент мне нужно включить свойство типа для декодирования массива, но для декодирования массива я должен знать его тип.

РЕДАКТИРОВАТЬ:

Мне понятно, почему 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

Ответ 1

Причина, по которой ваш первый пример не компилируется (и ваши второй аварий), потому что протоколы не соответствуют самим себе - Tag не является типом что соответствует Codable, поэтому и не [Tag]. Поэтому Article не получает автоматически сгенерированное соответствие Codable, так как не все его свойства соответствуют Codable.

Кодирование и декодирование только свойств, перечисленных в протоколе

Если вы просто хотите кодировать и декодировать свойства, перечисленные в протоколе, одним из решений было бы просто использовать стиратель типа 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 соответствующего типа, вы, скорее всего, захотите сохранить информацию о типе в 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
  }
}

Если вы знаете, что конфликта не будет, и хотите, чтобы свойства были закодированы/декодированы на том же уровне, что и ключ типа, так что JSON выглядит так:

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

Вы можете передать decoder вместо container.superDecoder(forKey: .base) и encoder вместо container.superEncoder(forKey: .base) в приведенном выше коде.

В качестве необязательного шага мы могли бы затем настроить реализацию Codable Article таким образом, чтобы вместо того, чтобы полагаться на автогенерированное соответствие с свойством tags, имеющим тип [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 тип, указанный в нашем перечислении 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")

Ответ 2

На основе принятого ответа я получил следующий код, который можно вставить в игровую площадку Xcode. Я использовал эту базу, чтобы добавить кодируемый протокол в мое приложение.

Вывод выглядит так, без вложенности, указанной в принятом ответе.

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

Ответ 3

Вдохновленный ответом @Hamish. Я нашел его подход разумным, однако кое-что можно улучшить:

  1. Массив сопоставления [Tag] и [AnyTag] в Article оставляет нас без автоматически сгенерированного соответствия Codable
  2. Невозможно иметь одинаковый код для массива кодирования/кодирования базового класса, поскольку static var type не может быть переопределен в подклассе. (например, если Tag будет суперклассом AuthorTag & GenreTag)
  3. Самое главное, этот код не может быть повторно использован для другого Типа, вам необходимо создать новую оболочку AnyAnotherType и ее внутреннюю кодировку/кодировку.

Я принял несколько иное решение, вместо того, чтобы оборачивать каждый элемент массива, можно сделать обертку на весь массив:

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

Это пример того, как кодировать/декодировать массив массивов для Swift 4. Большое спасибо, Алекс Гибсон.

import UIKit

struct Person: Codable {
  var name:String
}

class TestEncodeDecode: NSObject {

  func run() {

    // create
    let person1:Person = Person(name: "Joe")
    let person2:Person = Person(name: "Jay")
    let persons:[Person] = [person1, person2]

    // save
    let encoder = JSONEncoder()
    if let encoded = try? encoder.encode(persons) {
        UserDefaults.standard.set(encoded, forKey: "persons")
    }

    // load
    if let personsData = UserDefaults.standard.value(forKey: "persons") as? Data {
        let decoder = JSONDecoder()
        if let loadPersons = try? decoder.decode(Array.self, from: personsData) as [Person]{
            loadPersons.forEach { print($0) }
        }
    }
  }
}

Выход:

Person(name: "Joe")
Person(name: "Jay")