JSONEncoder를 사용한 프로토콜 준거 유형의 인코딩/복호화
Swift 4의 새로운 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"
}
]
}
문제는 어레이를 디코딩하려면 타입 속성을 켜야 하지만 어레이를 디코딩하려면 타입을 알아야 한다는 것입니다.
편집:
디코딩이 개봉 후 바로 작동하지 않는 이유는 분명하지만 적어도 인코딩은 작동해야 합니다.다음 수정된 아티클 구조가 컴파일되지만 다음 오류 메시지와 함께 충돌합니다.
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
첫 번째 예가 컴파일되지 않는 이유(및 두 번째 크래시)는 프로토콜이 자체와 맞지 않기 때문입니다.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
「CHANGE MARGE:」
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
}
}
그러나 경합이 발생하지 않고 속성을 "type" 키와 동일한 수준으로 인코딩/디코딩해야 하는 경우 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")
@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)
}
}
}
}
수락한 답변에서 도출한 결과, Xcode Playground에 붙여넣을 수 있는 다음과 같은 코드를 갖게 되었습니다.이 베이스를 사용하여 내 앱에 코드 가능한 프로토콜을 추가했습니다.
출력은 다음과 같습니다.허용된 답변에 기재되어 있는 네스트 없이 출력됩니다.
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()
태그의 종류에 대해서는 왜 에넘을 사용하지 않는 거죠?
struct Tag: Codable {
let type: TagType
let value: String
enum TagType: String, Codable {
case author
case genre
}
}
그러면 다음과 같이 인코딩할 수 있습니다.try? JSONEncoder().encode(tag)
또는 다음과 같이 디코딩합니다.let tags = try? JSONDecoder().decode([Tag].self, from: jsonData)
유형별로 태그를 필터링하는 등의 처리를 수행합니다.문서 구조에도 동일한 작업을 수행할 수 있습니다.
struct Tag: Codable {
let type: TagType
let value: String
enum TagType: String, Codable {
case author
case genre
}
}
struct Article: Codable {
let tags: [Tag]
let title: String
enum CodingKeys: String, CodingKey {
case tags
case title
}
}
저는 @Hamish로부터 인정받은 답변을 받아, 그것을 조금 일반화했습니다.다른 사람에게 도움이 될 수도 있으니까 여기에 올리면...
먼저 다음과 같은 재사용 가능한 유형을 설정합니다.AnyTag
그리고.TagType
.
protocol ConcreteTypeID: Codable {
var concreteType: any CodableExistential.Type { get }
}
protocol CodableExistential: Codable {
associatedtype TypeID: ConcreteTypeID
var concreteTypeId: TypeID { get }
}
struct ExistentialBox<TypeID: ConcreteTypeID>: Codable {
var existential: any CodableExistential
private enum CodingKey: Swift.CodingKey {
case concreteTypeId
}
init(_ existential: any CodableExistential) {
self.existential = existential
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKey.self)
let type = try container.decode(TypeID.self, forKey: .concreteTypeId)
self.existential = try type.concreteType.init(from: decoder)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKey.self)
try container.encode(existential.concreteTypeId, forKey: .concreteTypeId)
try existential.encode(to: encoder)
}
}
이제 당신의 구체적인 타입이 이것들을 활용하도록 하세요.
protocol Vehicle: CodableExistential {
var maker: String { get }
}
struct Car: Vehicle {
var concreteTypeId: VehicleTypeID { .car }
var maker: String
var numberOfPassengers: Int
}
struct Truck: Vehicle {
var concreteTypeId: VehicleTypeID { .truck }
var maker: String
}
enum VehicleTypeID: ConcreteTypeID {
case car, truck
var concreteType: any CodableExistential.Type {
switch self {
case .car:
return Car.self
case .truck:
return Truck.self
}
}
}
마지막으로 유형을 인코딩/복호화합니다.
struct Fleet: Codable {
var vehicles: [any Vehicle]
enum CodingKey: Swift.CodingKey { case vehicles }
init(vehicles: [any Vehicle]) {
self.vehicles = vehicles
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKey.self)
let boxes = try container.decode([ExistentialBox<VehicleTypeID>].self, forKey: .vehicles)
vehicles = boxes.map { $0.existential as! any Vehicle }
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKey.self)
let boxes = vehicles.map { ExistentialBox<VehicleTypeID>($0) }
try container.encode(boxes, forKey: .vehicles)
}
}
let fleet = Fleet(vehicles: [Car(maker: "Toyota", numberOfPassengers: 2), Truck(maker: "Mack")])
let data = try JSONEncoder().encode(fleet)
let unpackedFleet = try JSONDecoder().decode(Fleet.self, from: data)
나는 그 디코딩 방식에 사용된 캐스트에 대해 그다지 만족하지 않는다.Fleet
그러나 제네릭을 교체하여 이를 방지하려고 하면 "어떤 차량도 차량에 적합할 수 없습니다"와 같은 고전적인 오류가 발생합니다.만약 누군가 더 나은 방법을 찾을 수 있다면, 듣고 싶어요.
언급URL : https://stackoverflow.com/questions/44441223/encode-decode-array-of-types-conforming-to-protocol-with-jsonencoder
'programing' 카테고리의 다른 글
반응: 부모 구성 요소가 다시 렌더링될 때 어린이가 항상 다시 렌더링합니까? (0) | 2023.03.28 |
---|---|
'합성 속성 @panelState를 찾았습니다.응용 프로그램에 "Browser Animations Module" 또는 "Noop Animations Module"을 포함하십시오.' (0) | 2023.03.28 |
React 컴포넌트에 CSS 파일을 Import하는 방법 (0) | 2023.03.28 |
jquery AJAX 콜을 디버깅하려면 어떻게 해야 하나요? (0) | 2023.03.28 |
Angular를 사용한 W3C 검증JS 디렉티브 (0) | 2023.03.28 |