Кодирование/декодирование массивов типов, соответствующих протоколу с 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. Я нашел его подход разумным, однако кое-что можно улучшить:
- Массив сопоставления
[Tag]
и [AnyTag]
в Article
оставляет нас без автоматически сгенерированного соответствия Codable
- Невозможно иметь одинаковый код для массива кодирования/кодирования базового класса, поскольку
static var type
не может быть переопределен в подклассе. (например, если Tag
будет суперклассом AuthorTag
& GenreTag
) - Самое главное, этот код не может быть повторно использован для другого Типа, вам необходимо создать новую оболочку 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")