我正在try 使用swift 4中新的JSONDecoder/Encoder对符合swift协议的 struct 数组进行编码/解码的最佳方法.

我编了一个小例子来说明这个问题:

首先,我们有一个协议标签和一些符合该协议的类型.

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
}

然后我们有一个类型文章,它有一个标签array.

struct Article: Codable {
    let tags: [Tag]
    let title: String
}

Finally we encode or decode the Article

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)

And this is the JSON structure that I like to have.

{
 "title": "Article Title",
 "tags": [
     {
       "type": "author",
       "value": "Author Tag Value"
     },
     {
       "type": "genre",
       "value": "Genre Tag Value"
     }
 ]
}

The problem is that at some point I have to switch on the type property to decode the Array but to Decode the Array I have to know its type.

EDIT:

It's clear to me why Decodable can not work out of the box but at least Encodable should work. The following modified Article struct compiles but crashes with the following error message.

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中的相关部分.敏捷的

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

推荐答案

第一个示例没有编译(第二个示例崩溃)的原因是,protocols don't conform to themselvesTag不是符合Codable的类型,因此[Tag]也不是.因此,Article不会自动生成Codable一致性,因为并非其所有属性都符合Codable.

仅对协议中列出的属性进行编码和解码

If you just want to encode and decode the properties listed in the protocol, one solution would be to simply use an AnyTag type-eraser that just holds those properties, and can then provide the Codable conformance.

You can then have Article hold an array of this type-erased wrapper, rather than of 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"
    }
  ]
}

and can be decoded like so:

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

Encoding and decoding all properties of the conforming type

但是,如果您需要对给定的符合Tag的类型的every属性进行编码和解码,您可能希望以某种方式将类型信息存储在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))
    }
}

We're using a super encoder/decoder in order to ensure that the property keys for the given conforming type don't conflict with the key used to encode the type. For example, the encoded JSON will look like this:

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

If however you know there won't be a conflict, and want the properties to be encoded/decoded at the same level as the "type" key, such that the JSON looks like this:

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

在上面的代码中,您可以传递decoder而不是container.superDecoder(forKey: .base)&;encoder而不是container.superEncoder(forKey: .base).

作为optional步,然后我们可以定制ArticleCodable实现,这样,我们可以提供我们自己的实现,在编码之前将[Tag]打包成[AnyTag],然后解箱以进行解码,而不是依赖于类型为[AnyTag]tags属性的自动生成的一致性:

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].

现在,我们可以对TagType enum中列出的任何Tag个符合标准的类型进行编码和解码:

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

and can then be decoded like so:

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

Json相关问答推荐

合并二维数组的Jolt表达式

使用单元和非单元版本反序列化Rust中的枚举,而无需编写自定义反序列化程序

如何使用JQ将JSON字符串替换为解析后的类似功能?

解析SQL中的嵌套JSON

JQ-JSON将键转换为对象

用巨大的值更新SQL Server中的nvarchar(max)

用 Jolt 替换 JSON 中的值

Jolt 变换以展平 json 字符串数组

在 postgres 14 中将记录转换为所需的 json 格式

Golang 解组行为:字段过多?

golang递归json来构造?

如果 JSON 对象包含列表中的子字符串,则丢弃它们

在循环中将变量添加到 bash 数组

在 Perl Mojolicious 中呈现 JSON 时防止转义字符

如何使用 Newtonsoft.Json 反序列化 JSON 数组

如何在 Eclipse 中安装和使用 JSON 编辑器?

应该使用什么标头将 GZIP 压缩 JSON 从 Android 客户端发送到服务器?

使用 simplejson 序列化简单类对象的最简单方法?

将 PHP 结果数组转换为 JSON

在 React 中访问子级的父级状态