ejemplo - Codifique/Decodifique la matriz de tipos que se ajustan al protocolo con JSONEncoder
swift 4 codable json (4)
Estoy tratando de encontrar la mejor manera de codificar / decodificar una serie de estructuras que se ajusten a un protocolo Swift utilizando el nuevo codificador / codificador JSOND en Swift 4.
Inventé un pequeño ejemplo para ilustrar el problema:
Primero tenemos una etiqueta de protocolo y algunos tipos que se ajustan a este protocolo.
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
}
Luego tenemos un artículo de tipo que tiene una matriz de etiquetas.
struct Article: Codable {
let tags: [Tag]
let title: String
}
Finalmente codificamos o decodificamos el artículo.
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)
Y esta es la estructura JSON que me gusta tener.
{
"title": "Article Title",
"tags": [
{
"type": "author",
"value": "Author Tag Value"
},
{
"type": "genre",
"value": "Genre Tag Value"
}
]
}
El problema es que en algún momento tengo que encender la propiedad type para decodificar el Array pero Decodificar el Array si tengo que conocer su tipo.
EDITAR: Me queda claro por qué Decodable no puede funcionar fuera de la caja pero al menos Encodable debería funcionar. La siguiente estructura de artículo modificada compila pero se bloquea con el siguiente mensaje de error.
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)
Y esta es la parte relevante de 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.")
}
Fuente: https://github.com/apple/swift/blob/master/stdlib/public/core/Codable.swift
Este es un ejemplo de cómo codificar / decodificar la matriz de estructura para Swift 4. Muchas gracias Alex Gibson .
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) }
}
}
}
}
Salida:
Person(name: "Joe")
Person(name: "Jay")
Inspirado en la respuesta @Hamish. Encontré su enfoque razonable, sin embargo, pocas cosas podrían mejorarse:
- La matriz de asignación
[Tag]
[AnyTag]
desde[AnyTag]
en elArticle
nos deja sin conformidadCodable
generadaCodable
- No es posible tener el mismo código para codificar / codificar la matriz de la clase base, ya que
static var type
no se puede anular en la subclase. (por ejemplo, si laTag
sería súper clase deAuthorTag
yGenreTag
) - Lo más importante es que este código no se puede reutilizar para otro Tipo, se requiere que cree un nuevo contenedor Any AnotherType y que sea su codificación / codificación interna.
Hice una solución ligeramente diferente, en lugar de envolver cada elemento de la matriz, es posible hacer una envoltura en toda la matriz:
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)
}
}
}
Donde Meta
es el protocolo genérico:
protocol Meta: Codable {
associatedtype Element
static func metatype(for element: Element) -> Self
var type: Decodable.Type { get }
}
Ahora, guardar etiquetas se verá como:
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>
}
Resultado 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"
}
}
]
}
Y en caso de que quieras que JSON se vea aún más bonita:
{
"title" : "Article Title",
"tags" : [
{
"author" : {
"value" : "Author Tag Value"
}
},
{
"genre" : {
"value" : "Genre Tag Value"
}
}
]
}
Añadir al protocolo 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 }
}
Y reemplace CodingKeys
con:
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)
}
}
}
}
La razón por la que su primer ejemplo no se compila (y su segundo bloqueo) es porque los protocolos no se ajustan a sí mismos . La Tag
no es un tipo que se ajuste a Codable
, por lo tanto, tampoco lo es [Tag]
. Por lo tanto, el Article
no obtiene una conformidad Codable
, ya que no todas sus propiedades son conformes a Codable
.
Codificando y decodificando solo las propiedades listadas en el protocolo
Si solo desea codificar y decodificar las propiedades enumeradas en el protocolo, una solución sería simplemente usar un AnyTag
tipo AnyTag
que solo contenga esas propiedades, y luego pueda proporcionar la conformidad Codable
.
A continuación, puede hacer que el Article
contenga una matriz de este tipo de contenedor borrado, en lugar de la 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)
}
Que da salida a la siguiente cadena JSON:
{
"title" : "Article Title",
"tags" : [
{
"type" : "author",
"value" : "Author Tag Value"
},
{
"type" : "genre",
"value" : "Genre Tag Value"
}
]
}
y puede ser decodificado como tal:
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")
Codificación y decodificación de todas las propiedades del tipo conforme.
Sin embargo, si necesita codificar y decodificar todas las propiedades del tipo de conformidad con la Tag
dada, es probable que desee almacenar la información de tipo en el JSON de alguna manera.
Yo usaría una enum
para hacer esto:
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
}
}
}
Lo que es mejor que usar cadenas simples para representar los tipos, ya que el compilador puede verificar que hemos proporcionado un metatipo para cada caso.
Luego, solo tiene que cambiar el protocolo de la Tag
modo que requiera tipos conformes para implementar una propiedad static
que describa su tipo:
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
}
Luego necesitamos adaptar la implementación del envoltorio borrado de tipo para codificar y decodificar el TagType
junto con la Tag
base:
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))
}
}
Estamos utilizando un super codificador / decodificador para garantizar que las claves de propiedad para el tipo de conformidad dado no entren en conflicto con la clave utilizada para codificar el tipo. Por ejemplo, el JSON codificado se verá así:
{
"type" : "author",
"base" : {
"value" : "Author Tag Value",
"foo" : 56.7
}
}
Sin embargo, si sabe que no habrá un conflicto, y desea que las propiedades se codifiquen / descodifiquen al mismo nivel que la tecla "tipo", de modo que el JSON se vea así:
{
"type" : "author",
"value" : "Author Tag Value",
"foo" : 56.7
}
Puede pasar el decoder
lugar del container.superDecoder(forKey: .base)
y el encoder
lugar de container.superEncoder(forKey: .base)
en el código anterior.
Como paso opcional , podríamos personalizar la implementación Codable
del Article
tal manera que en lugar de confiar en una conformidad generada automáticamente con la propiedad de tags
de tipo [AnyTag]
, podemos proporcionar nuestra propia implementación que [AnyTag]
una [Tag]
en un [AnyTag]
antes de codificar, y luego descomprimir para decodificar:
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)
}
}
Esto nos permite tener la propiedad de las tags
de tipo [Tag]
, en lugar de [AnyTag]
.
Ahora podemos codificar y decodificar cualquier tipo de Tag
que esté listado en nuestra enumeración de 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)
}
Que da salida a la cadena 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"
}
}
]
}
y luego puede ser decodificado como tal:
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")
Tomado de la respuesta aceptada, terminé con el siguiente código que se puede pegar en un patio de juegos de Xcode. Utilicé esta base para agregar un protocolo codificable a mi aplicación.
La salida se ve así, sin el anidamiento mencionado en la respuesta aceptada.
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"
Pegue en su proyecto Xcode o Área de juegos y personalice a su gusto:
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()