jsondecoder ejemplo json swift encoding swift4

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:

  1. La matriz de asignación [Tag] [AnyTag] desde [AnyTag] en el Article nos deja sin conformidad Codable generada Codable
  2. 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 la Tag sería súper clase de AuthorTag y GenreTag )
  3. 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()