Кодировать / декодировать массив типов, соответствующих протоколу, с помощью 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 должен работать. Следующая измененная структура статьи компилируется, но дает сбой со следующим сообщением об ошибке.

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


person goesta    schedule 08.06.2017    source источник
comment
Зачем делать AuthorTag и GenreTag отдельные типы? У них обоих одинаковый интерфейс, и вы, кажется, просто используете свойство type, чтобы различать их (хотя на самом деле это, вероятно, должно быть enum).   -  person Hamish    schedule 08.06.2017
comment
Это просто упрощенный пример. У них могут быть индивидуальные свойства. Я тоже думал о том, чтобы сделать тип перечислением, но если тип является перечислением, я не могу добавлять новые типы без изменения перечисления.   -  person goesta    schedule 09.06.2017
comment
Действительно ли код работает и продукт JSON, который вы включили? Я получил Type 'Article' does not conform to protocol 'Decodable' и 'Encodable'   -  person Code Different    schedule 10.06.2017
comment
Я только что подтвердил на форуме Apple, что это ошибка. Пожалуйста, отправьте ее как ошибку   -  person Anish Parajuli 웃    schedule 10.06.2017
comment
@ThatlazyiOSGuy 웃 Я не понимаю, почему это ошибка - Tag не соответствует Codable (и, следовательно, тоже [Tag]), потому что протоколы не подчиняются себе. Подумайте, соответствует ли Tag Codable - что должно произойти при попытке декодера декодировать в произвольный Tag? Какой конкретный тип нужно создать?   -  person Hamish    schedule 10.06.2017
comment
@Hamish Если это так, компилятор не должен позволять протоколу соответствовать кодируемому   -  person Anish Parajuli 웃    schedule 10.06.2017
comment
@ThatlazyiOSGuy 웃 Верно, и это не так - код OP не компилируется.   -  person Hamish    schedule 10.06.2017
comment
Да, но если вы реализуете свои собственные методы Decodable и Encodable. Ошибку можно легко выбросить .. gist.github .com / anonymous / 74d2723e18444344f3635d403e8bf6b8 и это похоже на ошибку   -  person Anish Parajuli 웃    schedule 10.06.2017
comment
@ThatlazyiOSGuy 웃 А, я согласен, это похоже на ошибку (в том смысле, что он не должен компилироваться) :) Он вылетает во время выполнения, поскольку [Tag] не соответствует Codable.   -  person Hamish    schedule 10.06.2017
comment
Я добавил в вопрос кодируемый пример. @ThatlazyiOSGuy 웃 вы можете разместить ссылку на ветку форума Apple?   -  person goesta    schedule 10.06.2017
comment
@glektrik forum.developer.apple.com/message/234519#234519   -  person Anish Parajuli 웃    schedule 10.06.2017
comment
Я добавил запрос функции для этого поведения при ошибках Swift: bugs.swift.org/browse/SR- 7566   -  person pkamb    schedule 29.04.2018


Ответы (4)


Причина, по которой ваш первый пример не компилируется (а второй дает сбой), заключается в том, что протоколы не соответствуют самим себе - 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
  }
}

Однако если вы знаете, что конфликта не будет, и хотите, чтобы свойства кодировались / декодировались на том же уровне, что и ключ "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")
person Hamish    schedule 10.06.2017
comment
Вот это да. Пожалуйста, позвольте мне ничего не добавить к обсуждению, сказав, что это отличный ответ! - person zoul; 08.11.2017
comment
Я попробовал, и он отлично работает для создания объекта, но у меня проблема в том, что настраиваемые атрибуты объектов мешают декодеру. Я всегда получаю контейнер Cannot get keyed decoding - вместо этого обнаружил нулевое значение. Вы знаете здесь какую-нибудь помощь? Пример: foo var в AuthorTag, как только я добавляю эту строку, я получаю сообщение об ошибке. - person palme; 16.11.2017
comment
@palme А, вы, вероятно, используете логику декодирования, которая ожидает, что базовое значение будет закодировано как отдельный объект (под базовым ключом). Если вы ожидаете, что свойства базового значения будут на том же уровне, что и ключ типа, вы хотите передать decoder вместо container.superDecoder(forKey: .base) & encoder вместо container.superEncoder(forKey: .base) внутри логики декодирования / кодирования AnyTag. - person Hamish; 16.11.2017
comment
Еще раз спасибо за быстрый и точный ответ! Это сработало. - person palme; 16.11.2017
comment
Настоящее великолепие! - person matthewfx; 16.03.2018
comment
но как поступить, если Tag со связанным типом @Hamish - person kun wang; 24.04.2018
comment
@kunwang Боюсь, что для этого вам потребуется довольно сложное стирание типа. Вот примерный пример: gist.github.com/hamishknight/5ffe87a43590a1f1faedae341cf. - person Hamish; 01.05.2018
comment
Большое спасибо. Собирался опубликовать вопрос, когда StackOverflow предложил этот классный пост: D - person Freek Sanders; 24.12.2018
comment
Это помогает!!! Но я получил сообщение об ошибке: Protocol 'Tag' can only be used as a generic constraint because it has Self or associated type requirements, если я сделаю protocol Tag: Codable, Identifiable. Любая идея? Спасибо. - person Bagusflyer; 09.02.2020
comment
Чувак, это было круто. Все утро я искал элегантное решение. - person Lucas; 15.04.2021

Вдохновленный ответом @Hamish. Я нашел его подход разумным, однако кое-что можно улучшить:

  1. Сопоставление массива [Tag] с [AnyTag] в Article оставляет нас без автоматически сгенерированного Codable соответствия
  2. Невозможно иметь один и тот же код для массива кодирования / кодирования базового класса, поскольку static var type не может быть переопределен в подклассе. (например, если Tag будет суперклассом AuthorTag & GenreTag)
  3. Самое главное, что этот код нельзя повторно использовать для другого типа, вам необходимо создать новую оболочку Any AnotherType и ее внутреннюю кодировку / кодировку.

Я сделал немного другое решение, вместо того, чтобы оборачивать каждый элемент массива, можно сделать обертку для всего массива:

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)
            }
        }
    }
}
person Vadim Pavlov    schedule 24.10.2018

На основе принятого ответа я получил следующий код, который можно вставить в 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 или игровую площадку и настройте по своему вкусу:

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()
person pkamb    schedule 02.05.2018

Почему бы вам не использовать перечисления для типа тега?

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

person WizMeister    schedule 22.05.2020