0Pricing
Swift Academy · Урок

Декодирование неоднородного JSON

Обрабатывайте полиморфные и динамические структуры JSON.

«Декодирование неоднородного JSON» — бесплатный урок Swift Academy на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Swift Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Swift Academy содержит 4 уроков всего.

Что такое неоднородный JSON

Некоторые программные интерфейсы возвращают массивы, в которых объекты имеют разную структуру и различаются по полю type. Чтобы декодировать их в один тип Swift, сначала нужно проверить этот признак.

import Foundation

let json = "[{\"type\":\"text\",\"value\":\"hi\"},{\"type\":\"number\",\"value\":42}]"
print("Each element carries a type discriminator")

Моделирование с помощью перечисления

Естественная модель Swift — это перечисление со связанными значениями, по одному варианту для каждой структуры JSON. Декодер выберет вариант на основе признака.

import Foundation

enum Block {
    case text(String)
    case number(Int)
}

print("Enum models the variants")

Определение ключа-признака

Добавьте перечисление CodingKeys, включив в него поле-признак (здесь type) и необходимые ключи данных, которые нужно прочитать.

import Foundation

enum CodingKeys: String, CodingKey {
    case type
    case value
}

print(CodingKeys.type.stringValue, CodingKeys.value.stringValue)

Чтение признака

В init(from:) сначала декодируйте строку type, а затем используйте конструкцию выбора по её значению, чтобы декодировать соответствующие данные.

import Foundation

enum Block: Decodable {
    case text(String)
    case number(Int)
    enum CodingKeys: String, CodingKey { case type, value }
    init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        let type = try c.decode(String.self, forKey: .type)
        switch type {
        case "text": self = .text(try c.decode(String.self, forKey: .value))
        case "number": self = .number(try c.decode(Int.self, forKey: .value))
        default: throw DecodingError.dataCorruptedError(forKey: .type, in: c, debugDescription: "unknown type")
        }
    }
}

let b = try JSONDecoder().decode(Block.self, from: "{\"type\":\"text\",\"value\":\"hi\"}".data(using: .utf8)!)
if case let .text(s) = b { print(s) }

Декодирование смешанного массива

После того как перечисление стало Decodable, декодирование [Block].self одним вызовом обрабатывает целый массив элементов разной структуры.

import Foundation

enum Block: Decodable {
    case text(String)
    case number(Int)
    enum CodingKeys: String, CodingKey { case type, value }
    init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        switch try c.decode(String.self, forKey: .type) {
        case "text": self = .text(try c.decode(String.self, forKey: .value))
        case "number": self = .number(try c.decode(Int.self, forKey: .value))
        default: throw DecodingError.dataCorruptedError(forKey: .type, in: c, debugDescription: "unknown")
        }
    }
}

let json = "[{\"type\":\"text\",\"value\":\"hi\"},{\"type\":\"number\",\"value\":42}]"
let blocks = try JSONDecoder().decode([Block].self, from: json.data(using: .utf8)!)
print(blocks.count)

Сопоставление результата с вариантами

После декодирования переберите варианты перечисления, чтобы безопасно для типов выполнить нужное действие с каждым из них.

import Foundation

enum Block: Decodable {
    case text(String)
    case number(Int)
    enum CodingKeys: String, CodingKey { case type, value }
    init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        switch try c.decode(String.self, forKey: .type) {
        case "text": self = .text(try c.decode(String.self, forKey: .value))
        case "number": self = .number(try c.decode(Int.self, forKey: .value))
        default: throw DecodingError.dataCorruptedError(forKey: .type, in: c, debugDescription: "unknown")
        }
    }
}

let json = "[{\"type\":\"text\",\"value\":\"hi\"},{\"type\":\"number\",\"value\":42}]"
for b in try JSONDecoder().decode([Block].self, from: json.data(using: .utf8)!) {
    switch b {
    case .text(let s): print("text:", s)
    case .number(let n): print("number:", n)
    }
}

Вложенные данные

Если варианты содержат более сложные данные, декодируйте для каждого варианта отдельную вложенную структуру Codable, а не одно значение.

import Foundation

struct ImagePayload: Decodable { var url: String; var width: Int }
enum Block: Decodable {
    case image(ImagePayload)
    enum CodingKeys: String, CodingKey { case type }
    init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        switch try c.decode(String.self, forKey: .type) {
        case "image": self = .image(try ImagePayload(from: decoder))
        default: throw DecodingError.dataCorruptedError(forKey: .type, in: c, debugDescription: "unknown")
        }
    }
}

let json = "{\"type\":\"image\",\"url\":\"a.png\",\"width\":100}"
if case let .image(p) = try JSONDecoder().decode(Block.self, from: json.data(using: .utf8)!) {
    print(p.url, p.width)
}

Корректная обработка неизвестных типов

Вместо того чтобы выбрасывать ошибку при неизвестном признаке, можно сопоставить его с запасным вариантом, чтобы новые типы на сервере никогда не приводили к сбою клиента.

import Foundation

enum Block: Decodable {
    case text(String)
    case unknown
    enum CodingKeys: String, CodingKey { case type, value }
    init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        switch try c.decode(String.self, forKey: .type) {
        case "text": self = .text(try c.decode(String.self, forKey: .value))
        default: self = .unknown
        }
    }
}

let b = try JSONDecoder().decode(Block.self, from: "{\"type\":\"video\"}".data(using: .utf8)!)
if case .unknown = b { print("fell back to unknown") }

Выбор хорошего признака

Признак должен быть стабильным и обязательным полем. Перечисление String с известными именами типов делает конструкцию выбора исчерпывающей и понятной.

import Foundation

enum Kind: String, Decodable { case text, number }

let data = "\"text\"".data(using: .utf8)!
let k = try JSONDecoder().decode(Kind.self, from: data)
print(k == .text)

Кодирование полиморфных значений

Чтобы обеспечить обратное преобразование, реализуйте также encode(to:): запишите признак и данные соответствующего варианта.

import Foundation

enum Block: Encodable {
    case text(String)
    case number(Int)
    enum CodingKeys: String, CodingKey { case type, value }
    func encode(to encoder: Encoder) throws {
        var c = encoder.container(keyedBy: CodingKeys.self)
        switch self {
        case .text(let s):
            try c.encode("text", forKey: .type)
            try c.encode(s, forKey: .value)
        case .number(let n):
            try c.encode("number", forKey: .type)
            try c.encode(n, forKey: .value)
        }
    }
}

let data = try JSONEncoder().encode(Block.number(42))
print(String(data: data, encoding: .utf8)!)

Собираем всё вместе

Полная полиморфная модель декодирует смешанный массив, выбирает действие для каждого варианта и может снова закодировать данные в той же структуре — это надёжный шаблон для гибких программных интерфейсов.

import Foundation

enum Block: Codable {
    case text(String)
    case number(Int)
    enum CodingKeys: String, CodingKey { case type, value }
    init(from d: Decoder) throws {
        let c = try d.container(keyedBy: CodingKeys.self)
        switch try c.decode(String.self, forKey: .type) {
        case "text": self = .text(try c.decode(String.self, forKey: .value))
        default: self = .number(try c.decode(Int.self, forKey: .value))
        }
    }
    func encode(to e: Encoder) throws {
        var c = e.container(keyedBy: CodingKeys.self)
        switch self {
        case .text(let s): try c.encode("text", forKey: .type); try c.encode(s, forKey: .value)
        case .number(let n): try c.encode("number", forKey: .type); try c.encode(n, forKey: .value)
        }
    }
}

let blocks: [Block] = [.text("hi"), .number(7)]
let data = try JSONEncoder().encode(blocks)
print(try JSONDecoder().decode([Block].self, from: data).count)

Проверка: неоднородный JSON

Проверьте, насколько хорошо Вы поняли полиморфное декодирование.

Итоги: декодирование неоднородного JSON

Вы научились обрабатывать полиморфные данные:

  • Моделируйте варианты как перечисление со связанными значениями.
  • В init(from:) декодируйте поле-признак, а затем выбирайте нужный вариант.
  • Декодируйте смешанные массивы с помощью [Enum].self и сопоставляйте результаты с вариантами.
  • Добавляйте запасной вариант для неизвестных типов и реализуйте encode(to:) для обратного преобразования.

Часто задаваемые вопросы

Урок «Декодирование неоднородного JSON» бесплатный?

Да — полный текст урока «Декодирование неоднородного JSON» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Swift Academy, подпишись на CoddyKit PRO. Курс Swift Academy содержит 4 уроков всего.

Чему я научусь в уроке «Декодирование неоднородного JSON»?

Обрабатывайте полиморфные и динамические структуры JSON. Ты практикуешь Swift Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать Swift Academy?

Предыдущий опыт не требуется. Swift Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.

Сколько времени занимает урок «Декодирование неоднородного JSON»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке Swift Academy?

Да. Каждый урок Swift Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. CodingKeys для переименования
  2. Стратегии декодирования ключей и дат
  3. Ручные encode(to:) и init(from:)
  4. Декодирование неоднородного JSON
← Назад к Swift Academy