Как декодировать свойство с типом JSON-словаря в Swift [45] протокол decodable

Вопрос или проблема

Предположим, у меня есть Customer тип данных, который содержит свойство metadata, которое может содержать любой JSON-словарь в объекте клиента.

struct Customer {
  let id: String
  let email: String
  let metadata: [String: Any]
}

{
  "object": "customer",
  "id": "4yq6txdpfadhbaqnwp3",
  "email": "[email protected]",
  "metadata": {
    "link_id": "linked-id",
    "buy_count": 4
  }
}

Свойство metadata может быть любым произвольным объектом JSON.

Ранее я мог привязать это свойство от десериализованного JSON из NSJSONDeserialization, но с новым протоколом Swift 4 Decodable я все еще не могу придумать, как это сделать.

Знает ли кто-нибудь, как достичь этого в Swift 4 с протоколом Decodable?

С некоторой вдохновением из этого гиста, я написал несколько расширений для UnkeyedDecodingContainer и KeyedDecodingContainer. Вы можете найти ссылку на мой гист здесь. Используя этот код, теперь вы можете декодировать любой Array<Any> или Dictionary<String, Any> с привычным синтаксисом:

let dictionary: [String: Any] = try container.decode([String: Any].self, forKey: key)

или

let array: [Any] = try container.decode([Any].self, forKey: key)

Редактировать: существует одно ограничение, которое я нашел, это декодирование массива словарей [[String: Any]]. Необходимый синтаксис следующий. Скорее всего, вы захотите выбросить ошибку вместо принудительного приведения:

let items: [[String: Any]] = try container.decode(Array<Any>.self, forKey: .items) as! [[String: Any]]

EDIT 2: Если вы просто хотите конвертировать весь файл в словарь, вам лучше продолжать использовать api от JSONSerialization, так как я не нашел способа расширить JSONDecoder, чтобы напрямую декодировать словарь.

guard let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
  // соответствующая обработка ошибок
  return
}

Расширения

// Вдохновлено https://gist.github.com/mbuchetics/c9bc6c22033014aa0c550d3b4324411a

struct JSONCodingKeys: CodingKey {
    var stringValue: String

    init?(stringValue: String) {
        self.stringValue = stringValue
    }

    var intValue: Int?

    init?(intValue: Int) {
        self.init(stringValue: "\(intValue)")
        self.intValue = intValue
    }
}

extension KeyedDecodingContainer {

    func decode(_ type: Dictionary<String, Any>.Type, forKey key: K) throws -> Dictionary<String, Any> {
        let container = try self.nestedContainer(keyedBy: JSONCodingKeys.self, forKey: key)
        return try container.decode(type)
    }

    func decodeIfPresent(_ type: Dictionary<String, Any>.Type, forKey key: K) throws -> Dictionary<String, Any>? {
        guard contains(key) else { 
            return nil
        }
        guard try decodeNil(forKey: key) == false else { 
            return nil 
        }
        return try decode(type, forKey: key)
    }

    func decode(_ type: Array<Any>.Type, forKey key: K) throws -> Array<Any> {
        var container = try self.nestedUnkeyedContainer(forKey: key)
        return try container.decode(type)
    }

    func decodeIfPresent(_ type: Array<Any>.Type, forKey key: K) throws -> Array<Any>? {
        guard contains(key) else {
            return nil
        }
        guard try decodeNil(forKey: key) == false else { 
            return nil 
        }
        return try decode(type, forKey: key)
    }

    func decode(_ type: Dictionary<String, Any>.Type) throws -> Dictionary<String, Any> {
        var dictionary = Dictionary<String, Any>()

        for key in allKeys {
            if let boolValue = try? decode(Bool.self, forKey: key) {
                dictionary[key.stringValue] = boolValue
            } else if let stringValue = try? decode(String.self, forKey: key) {
                dictionary[key.stringValue] = stringValue
            } else if let intValue = try? decode(Int.self, forKey: key) {
                dictionary[key.stringValue] = intValue
            } else if let doubleValue = try? decode(Double.self, forKey: key) {
                dictionary[key.stringValue] = doubleValue
            } else if let nestedDictionary = try? decode(Dictionary<String, Any>.self, forKey: key) {
                dictionary[key.stringValue] = nestedDictionary
            } else if let nestedArray = try? decode(Array<Any>.self, forKey: key) {
                dictionary[key.stringValue] = nestedArray
            }
        }
        return dictionary
    }
}

extension UnkeyedDecodingContainer {

    mutating func decode(_ type: Array<Any>.Type) throws -> Array<Any> {
        var array: [Any] = []
        while isAtEnd == false {
            // Сначала смотрим, является ли текущее значение в массиве JSON `null` и предотвращаем бесконечную рекурсию с вложенными массивами.
            if try decodeNil() {
                continue
            } else if let value = try? decode(Bool.self) {
                array.append(value)
            } else if let value = try? decode(Double.self) {
                array.append(value)
            } else if let value = try? decode(String.self) {
                array.append(value)
            } else if let nestedDictionary = try? decode(Dictionary<String, Any>.self) {
                array.append(nestedDictionary)
            } else if let nestedArray = try? decode(Array<Any>.self) {
                array.append(nestedArray)
            }
        }
        return array
    }

    mutating func decode(_ type: Dictionary<String, Any>.Type) throws -> Dictionary<String, Any> {

        let nestedContainer = try self.nestedContainer(keyedBy: JSONCodingKeys.self)
        return try nestedContainer.decode(type)
    }
}

Я тоже занимался этой проблемой и в конце концов написал простую библиотеку для работы с “универсальными JSON” типами. (Где “универсальный” означает “без заранее известной структуры”.) Основная идея заключается в том, чтобы представить универсальный JSON с конкретным типом:

public enum JSON {
    case string(String)
    case number(Float)
    case object([String:JSON])
    case array([JSON])
    case bool(Bool)
    case null
}

Этот тип может затем реализовывать Codable и Equatable.

Я пришел к немного другому решению.

Предположим, у нас есть что-то большее, чем простой [String: Any] для разбора, где Any может быть массивом или вложенным словарем или словарем массивов.

Что-то вроде этого:

var json = """
{
  "id": 12345,
  "name": "Giuseppe",
  "last_name": "Lanza",
  "age": 31,
  "happy": true,
  "rate": 1.5,
  "classes": ["maths", "phisics"],
  "dogs": [
    {
      "name": "Gala",
      "age": 1
    }, {
      "name": "Aria",
      "age": 3
    }
  ]
}
"""

Что ж, вот моё решение:

public struct AnyDecodable: Decodable {
  public var value: Any

  private struct CodingKeys: CodingKey {
    var stringValue: String
    var intValue: Int?
    init?(intValue: Int) {
      self.stringValue = "\(intValue)"
      self.intValue = intValue
    }
    init?(stringValue: String) { self.stringValue = stringValue }
  }

  public init(from decoder: Decoder) throws {
    if let container = try? decoder.container(keyedBy: CodingKeys.self) {
      var result = [String: Any]()
      try container.allKeys.forEach { (key) throws in
        result[key.stringValue] = try container.decode(AnyDecodable.self, forKey: key).value
      }
      value = result
    } else if var container = try? decoder.unkeyedContainer() {
      var result = [Any]()
      while !container.isAtEnd {
        result.append(try container.decode(AnyDecodable.self).value)
      }
      value = result
    } else if let container = try? decoder.singleValueContainer() {
      if let intVal = try? container.decode(Int.self) {
        value = intVal
      } else if let doubleVal = try? container.decode(Double.self) {
        value = doubleVal
      } else if let boolVal = try? container.decode(Bool.self) {
        value = boolVal
      } else if let stringVal = try? container.decode(String.self) {
        value = stringVal
      } else {
        throw DecodingError.dataCorruptedError(in: container, debugDescription: "контейнер не содержит ничего сериализуемого")
      }
    } else {
      throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Не удалось сериализовать"))
    }
  }
}

Попробуйте это, используя

let stud = try! JSONDecoder().decode(AnyDecodable.self, from: jsonData).value as! [String: Any]
print(stud)

Вы можете создать структуру metadata, которая соответствует протоколу Decodable и использовать класс JSONDecoder для создания объекта из данных, используя метод decode следующим образом:

let json: [String: Any] = [
    "object": "customer",
    "id": "4yq6txdpfadhbaqnwp3",
    "email": "[email protected]",
    "metadata": [
        "link_id": "linked-id",
        "buy_count": 4
    ]
]

struct Customer: Decodable {
    let object: String
    let id: String
    let email: String
    let metadata: Metadata
}

struct Metadata: Decodable {
    let link_id: String
    let buy_count: Int
}

let data = try JSONSerialization.data(withJSONObject: json, options: .prettyPrinted)

let decoder = JSONDecoder()
do {
    let customer = try decoder.decode(Customer.self, from: data)
    print(customer)
} catch {
    print(error.localizedDescription)
}

Если вы используете SwiftyJSON для разбора JSON, вы можете обновиться до 4.1.0, которая поддерживает протокол Codable. Просто объявите metadata: JSON, и все готово.

import SwiftyJSON

struct Customer {
  let id: String
  let email: String
  let metadata: JSON
}

Когда я нашел старый ответ, я протестировал только простой случай с JSON объектом, но не пустой, что приведет к исключению времени выполнения, как обнаружили @slurmomatic и @zoul. Извините за эту проблему.

Поэтому я попробовал другой способ, создав простой протокол JSONValue, реализовав тип AnyJSONValue типа стирания и используя этот тип вместо Any. Вот реализация.

public protocol JSONType: Decodable {
    var jsonValue: Any { get }
}

extension Int: JSONType {
    public var jsonValue: Any { return self }
}
extension String: JSONType {
    public var jsonValue: Any { return self }
}
extension Double: JSONType {
    public var jsonValue: Any { return self }
}
extension Bool: JSONType {
    public var jsonValue: Any { return self }
}

public struct AnyJSONType: JSONType {
    public let jsonValue: Any

    public init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()

        if let intValue = try? container.decode(Int.self) {
            jsonValue = intValue
        } else if let stringValue = try? container.decode(String.self) {
            jsonValue = stringValue
        } else if let boolValue = try? container.decode(Bool.self) {
            jsonValue = boolValue
        } else if let doubleValue = try? container.decode(Double.self) {
            jsonValue = doubleValue
        } else if let doubleValue = try? container.decode(Array<AnyJSONType>.self) {
            jsonValue = doubleValue
        } else if let doubleValue = try? container.decode(Dictionary<String, AnyJSONType>.self) {
            jsonValue = doubleValue
        } else {
            throw DecodingError.typeMismatch(JSONType.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Несоответствующий тип JSON"))
        }
    }
}

А вот как это использовать при декодировании:

metadata = try container.decode ([String: AnyJSONValue].self, forKey: .metadata)

Проблема с этим решением заключается в том, что мы должны вызывать value.jsonValue as? Int. Нам нужно дождаться, пока Conditional Conformance появится в Swift, это решит проблему или хотя бы поможет лучше.


[Старый ответ]

Я опубликовал этот вопрос на форуме разработчиков Apple, и оказалось, что это очень просто.

Я могу сделать:

metadata = try container.decode ([String: Any].self, forKey: .metadata)

в инициализаторе.

Я сам виноват, что пропустил это в первую очередь.

Детали

  • Xcode 12.0.1 (12A7300)
  • Swift 5.3

На основе библиотеки Tai Le

// код от: https://github.com/levantAJ/AnyCodable/blob/master/AnyCodable/DecodingContainer%2BAnyCollection.swift

private
struct AnyCodingKey: CodingKey {
    let stringValue: String
    private (set) var intValue: Int?
    init?(stringValue: String) { self.stringValue = stringValue }
    init?(intValue: Int) {
        self.intValue = intValue
        stringValue = String(intValue)
    }
}

extension KeyedDecodingContainer {

    private
    func decode(_ type: [Any].Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> [Any] {
        var values = try nestedUnkeyedContainer(forKey: key)
        return try values.decode(type)
    }

    private
    func decode(_ type: [String: Any].Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> [String: Any] {
        try nestedContainer(keyedBy: AnyCodingKey.self, forKey: key).decode(type)
    }

    func decode(_ type: [String: Any].Type) throws -> [String: Any] {
        var dictionary: [String: Any] = [:]
        for key in allKeys {
            if try decodeNil(forKey: key) {
                dictionary[key.stringValue] = NSNull()
            } else if let bool = try? decode(Bool.self, forKey: key) {
                dictionary[key.stringValue] = bool
            } else if let string = try? decode(String.self, forKey: key) {
                dictionary[key.stringValue] = string
            } else if let int = try? decode(Int.self, forKey: key) {
                dictionary[key.stringValue] = int
            } else if let double = try? decode(Double.self, forKey: key) {
                dictionary[key.stringValue] = double
            } else if let dict = try? decode([String: Any].self, forKey: key) {
                dictionary[key.stringValue] = dict
            } else if let array = try? decode([Any].self, forKey: key) {
                dictionary[key.stringValue] = array
            }
        }
        return dictionary
    }
}

extension UnkeyedDecodingContainer {
    mutating func decode(_ type: [Any].Type) throws -> [Any] {
        var elements: [Any] = []
        while !isAtEnd {
            if try decodeNil() {
                elements.append(NSNull())
            } else if let int = try? decode(Int.self) {
                elements.append(int)
            } else if let bool = try? decode(Bool.self) {
                elements.append(bool)
            } else if let double = try? decode(Double.self) {
                elements.append(double)
            } else if let string = try? decode(String.self) {
                elements.append(string)
            } else if let values = try? nestedContainer(keyedBy: AnyCodingKey.self),
                let element = try? values.decode([String: Any].self) {
                elements.append(element)
            } else if var values = try? nestedUnkeyedContainer(),
                let element = try? values.decode([Any].self) {
                elements.append(element)
            }
        }
        return elements
    }
}

Решение

struct DecodableDictionary: Decodable {
    typealias Value = [String: Any]
    let dictionary: Value?
    init(from decoder: Decoder) throws {
        dictionary = try? decoder.container(keyedBy: AnyCodingKey.self).decode(Value.self)
    }
}

Использование

struct Model: Decodable {
    let num: Double?
    let flag: Bool?
    let dict: DecodableDictionary?
    let dict2: DecodableDictionary?
    let dict3: DecodableDictionary?
}

let data = try! JSONSerialization.data(withJSONObject: dictionary)
let object = try JSONDecoder().decode(Model.self, from: data)
print(object.dict?.dictionary)      // печатает [String: Any]
print(object.dict2?.dictionary)     // печатает nil
print(object.dict3?.dictionary)     // печатает nil

Возможно, вам стоит взглянуть на BeyovaJSON

import BeyovaJSON

struct Customer: Codable {
  let id: String
  let email: String
  let metadata: JToken
}

//создайте экземпляр клиента

customer.metadata = ["link_id": "linked-id","buy_count": 4]

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted 
print(String(bytes: try! encoder.encode(customer), encoding: .utf8)!)

Вот более универсальный (не только [String: Any], но [Any] может быть декодирован) и инкапсулированный подход (для этого используется отдельная сущность), вдохновленный ответом @loudmouth.

Использование будет выглядеть так:

extension Customer: Decodable {
  public init(from decoder: Decoder) throws {
    let selfContainer = try decoder.container(keyedBy: CodingKeys.self)
    id = try selfContainer.decode(.id)
    email = try selfContainer.decode(.email)
    let metadataContainer: JsonContainer = try selfContainer.decode(.metadata)
    guard let metadata = metadataContainer.value as? [String: Any] else {
      let context = DecodingError.Context(codingPath: [CodingKeys.metadata], debugDescription: "Ожидался '[String: Any]' для ключа 'metadata'")
      throw DecodingError.typeMismatch([String: Any].self, context)
    }
    self.metadata = metadata
  }

  private enum CodingKeys: String, CodingKey {
    case id, email, metadata
  }
}

JsonContainer является вспомогательной сущностью, которую мы используем для упаковки декодирования данных JSON в объект JSON (либо массив, либо словарь) без расширения *DecodingContainer (чтобы это не мешало редким случаям, когда объект JSON не предназначен для [String: Any]).

struct JsonContainer {

  let value: Any
}

extension JsonContainer: Decodable {

  public init(from decoder: Decoder) throws {
    if let keyedContainer = try? decoder.container(keyedBy: Key.self) {
      var dictionary = [String: Any]()
      for key in keyedContainer.allKeys {
        if let value = try? keyedContainer.decode(Bool.self, forKey: key) {
          // Упаковка числовых и булевых типов в `NSNumber` важна, поэтому будут работать приведения как `as? Int64` или `as? Float`
          dictionary[key.stringValue] = NSNumber(value: value)
        } else if let value = try? keyedContainer.decode(Int64.self, forKey: key) {
          dictionary[key.stringValue] = NSNumber(value: value)
        } else if let value = try? keyedContainer.decode(Double.self, forKey: key) {
          dictionary[key.stringValue] = NSNumber(value: value)
        } else if let value = try? keyedContainer.decode(String.self, forKey: key) {
          dictionary[key.stringValue] = value
        } else if (try? keyedContainer.decodeNil(forKey: key)) ?? false {
          // НОД
        } else if let value = try? keyedContainer.decode(JsonContainer.self, forKey: key) {
          dictionary[key.stringValue] = value.value
        } else {
          throw DecodingError.dataCorruptedError(forKey: key, in: keyedContainer, debugDescription: "Неожиданное значение для ключа \(key.stringValue)")
        }
      }
      value = dictionary
    } else if var unkeyedContainer = try? decoder.unkeyedContainer() {
      var array = [Any]()
      while !unkeyedContainer.isAtEnd {
        let container = try unkeyedContainer.decode(JsonContainer.self)
        array.append(container.value)
      }
      value = array
    } else if let singleValueContainer = try? decoder.singleValueContainer() {
      if let value = try? singleValueContainer.decode(Bool.self) {
        self.value = NSNumber(value: value)
      } else if let value = try? singleValueContainer.decode(Int64.self) {
        self.value = NSNumber(value: value)
      } else if let value = try? singleValueContainer.decode(Double.self) {
        self.value = NSNumber(value: value)
      } else if let value = try? singleValueContainer.decode(String.self) {
        self.value = value
      } else if singleValueContainer.decodeNil() {
        value = NSNull()
      } else {
        throw DecodingError.dataCorruptedError(in: singleValueContainer, debugDescription: "Неожиданное значение")
      }
    } else {
      let context = DecodingError.Context(codingPath: [], debugDescription: "Неверный формат данных для JSON")
      throw DecodingError.dataCorrupted(context)
    }
  }

  private struct Key: CodingKey {
    var stringValue: String

    init?(stringValue: String) {
      self.stringValue = stringValue
    }

    var intValue: Int?

    init?(intValue: Int) {
      self.init(stringValue: "\(intValue)")
      self.intValue = intValue
    }
  }
}

Обратите внимание, что числовые и булевы типы поддерживаются через NSNumber, в противном случае что-то вроде этого не сработает:

if customer.metadata["keyForInt"] as? Int64 { // так как это всегда будет nil

Я создал pod, чтобы упростить процесс декодирования + кодирования [String: Any], [Any]. И это обеспечивает кодирование или декодирование опциональных свойств, вот здесь https://github.com/levantAJ/AnyCodable

pod 'DynamicCodable', '1.0'

Как это использовать:

import DynamicCodable

struct YourObject: Codable {
    var dict: [String: Any]
    var array: [Any]
    var optionalDict: [String: Any]?
    var optionalArray: [Any]?

    enum CodingKeys: String, CodingKey {
        case dict
        case array
        case optionalDict
        case optionalArray
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        dict = try values.decode([String: Any].self, forKey: .dict)
        array = try values.decode([Any].self, forKey: .array)
        optionalDict = try values.decodeIfPresent([String: Any].self, forKey: .optionalDict)
        optionalArray = try values.decodeIfPresent([Any].self, forKey: .optionalArray)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(dict, forKey: .dict)
        try container.encode(array, forKey: .array)
        try container.encodeIfPresent(optionalDict, forKey: .optionalDict)
        try container.encodeIfPresent(optionalArray, forKey: .optionalArray)
    }
}

Я использовал некоторые из ответов на эту тему, чтобы получить как можно более простое решение. Моя проблема заключалась в том, что я получал словарь типа [String: Any], но вполне мог бы работать со [String: String], преобразовывая каждое другое значение Any в String. Так что вот моё решение:

struct MetadataType: Codable {
    let value: String?

    private init(_ value: String?) {
        self.value = value
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()

        if let decodedValue = try? container.decode(Int.self) {
            self.init(String(decodedValue))
        } else if let decodedValue = try? container.decode(Double.self) {
            self.init(String(decodedValue))
        } else if let decodedValue = try? container.decode(Bool.self) {
            self.init(String(decodedValue))
        } else if let decodedValue = try? container.decode(String.self) {
            self.init(decodedValue)
        } else {
            self.init(nil)
        }
    }
}

И при объявлении моего словаря я использую

let userInfo: [String: MetadataType]

Я написал статью и репозиторий, который помогает в добавлении поддержки [String: Any] для Codable как для декодирования, так и для кодирования.

https://medium.com/nerd-for-tech/string-any-support-for-codable-4ba062ce62f2

Это улучшает аспект декодирования и также добавляет поддержку кодирования, как это предложено в https://stackoverflow.com/a/46049763/9160905

что вы сможете достичь:

json:

enter image description here

пример кода:

enter image description here

Самый простой и рекомендуемый способ – это создать отдельную модель для каждого словаря или модели, которая находится в JSON.

Вот что я делаю

//Модель для словаря **Metadata**

struct Metadata: Codable {
    var link_id: String?
    var buy_count: Int?
}  

//Модель для словаря **Customer**

struct Customer: Codable {
   var object: String?
   var id: String?
   var email: String?
   var metadata: Metadata?
}

//Вот наш декодируемый парсер, который декодирует JSON в ожидаемую модель

struct CustomerParser {
    var customer: Customer?
}

extension CustomerParser: Decodable {

//ключи, которые совпадают точно с JSON
enum CustomerKeys: String, CodingKey {
    case object = "object"
    case id = "id"
    case email = "email"
    case metadata = "metadata"
}

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CustomerKeys.self) // определяем наш (ключевой) контейнер

    let object: String = try container.decode(String.self, forKey: .object) // извлекаем данные
    let id: String = try container.decode(String.self, forKey: .id) // извлекаем данные
    let email: String = try container.decode(String.self, forKey: .email) // извлекаем данные

   //Здесь я использовал модель metadata вместо словаря [String: Any]
    let metadata: Metadata = try container.decode(Metadata.self, forKey: .metadata) // извлекаем данные

    self.init(customer: Customer(object: object, id: id, email: email, metadata: metadata))

    }
}

Использование:

  if let url = Bundle.main.url(forResource: "customer-json-file", withExtension: "json") {
        do {
            let jsonData: Data =  try Data(contentsOf: url)
            let parser: CustomerParser = try JSONDecoder().decode(CustomerParser.self, from: jsonData)
            print(parser.customer ?? "null")

        } catch {

        }
    }

**Я использовал опционал, чтобы быть в безопасной стороне во время разбора, это можно изменить по мере необходимости.

Читать подробнее на эту тему

декодировать с использованием декодера и ключей кодирования

public let dataToDecode: [String: AnyDecodable]

enum CodingKeys: CodingKey {
    case dataToDecode
}

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.dataToDecode = try container.decode(Dictionary<String, AnyDecodable>.self, forKey: .dataToDecode) 
}    

Это будет работать

public struct AnyDecodable: Decodable {
    public let value: Any

    public init<T>(_ value: T?) {
        self.value = value ?? ()
    }
}

let contentDecodable = try values.decodeIfPresent(AnyDecodable.self, forKey: .content)
extension ViewController {

    func swiftyJson(){
        let url = URL(string: "https://itunes.apple.com/search?term=jack+johnson")
        //let url = URL(string: "http://makani.bitstaging.in/api/business/businesses_list")

        Alamofire.request(url!, method: .get, parameters: nil).responseJSON { response in
            var arrayIndexes = [IndexPath]()
            switch(response.result) {
            case .success(_):

                let data = response.result.value as! [String : Any]

                if let responseData =  Mapper<DataModel>().map(JSON: data) {
                    if responseData.results!.count > 0{
                        self.arrayExploreStylistList = []
                    }
                    for i in 0..<responseData.results!.count{
                        arrayIndexes.append(IndexPath(row: self.arrayExploreStylistList.count + i, section: 0))
                    }
                    self.arrayExploreStylistList.append(contentsOf: responseData.results!)

                    print(arrayIndexes.count)

                }

                //                    if let arrNew = data["results"] as? [[String : Any]]{
                //                        let jobData = Mapper<DataModel>().mapArray(JSONArray: arrNew)
                //                        print(jobData)
                //                        self.datamodel = jobData
                //                    }
                self.tblView.reloadData()
                break

            case .failure(_):
                print(response.result.error as Any)
                break

            }
        }

    }
}

Ответ или решение

Вопрос: как декодировать свойство типа JSON-словарь в Swift, используя протокол Decodable, когда это свойство может содержать произвольный JSON-объект?

Введение

При работе с JSON в Swift вы часто сталкиваетесь с задачами декодирования данных в структурированные модели. Проблема возникает, когда одно из свойств, например, metadata, может содержать произвольные данные в виде словаря, что делает его сложным для декодирования. В этой статье мы рассмотрим, как можно добиться желаемого результата, используя протокол Decodable и некоторые расширения для него.

Шаг 1: Определение структуры

Допустим, у вас есть следующая структура Customer:

struct Customer: Decodable {
    let id: String
    let email: String
    let metadata: [String: Any] // Проблемное свойство
}

Шаг 2: Изменение структуры метаданных

Поскольку Swift не может декодировать [String: Any] напрямую, мы создадим структуру AnyDecodable, которая будет использоваться для хранения всех возможных типов данных, которые могут встретиться в вашем JSON.

public struct AnyDecodable: Decodable {
    public var value: Any

    private struct CodingKeys: CodingKey {
        var stringValue: String
        var intValue: Int?

        init?(intValue: Int) {
            self.stringValue = "\(intValue)"
        }

        init?(stringValue: String) {
            self.stringValue = stringValue
        }
    }

    public init(from decoder: Decoder) throws {
        if let container = try? decoder.container(keyedBy: CodingKeys.self) {
            var result = [String: Any]()
            for key in container.allKeys {
                result[key.stringValue] = try container.decode(AnyDecodable.self, forKey: key).value
            }
            value = result
        } else if var container = try? decoder.unkeyedContainer() {
            var result = [Any]()
            while !container.isAtEnd {
                result.append(try container.decode(AnyDecodable.self).value)
            }
            value = result
        } else {
            let singleValueContainer = try decoder.singleValueContainer()
            if let bool = try? singleValueContainer.decode(Bool.self) {
                value = bool
            } else if let int = try? singleValueContainer.decode(Int.self) {
                value = int
            } else if let double = try? singleValueContainer.decode(Double.self) {
                value = double
            } else if let string = try? singleValueContainer.decode(String.self) {
                value = string
            } else {
                throw DecodingError.dataCorruptedError(in: singleValueContainer, debugDescription: "Unsupported JSON type")
            }
        }
    }
}

Шаг 3: Реализация структуры Customer

Теперь мы можем адаптировать нашу структуру Customer, чтобы использовать AnyDecodable для обработки metadata.

struct Customer: Decodable {
    let id: String
    let email: String
    let metadata: [String: Any] // Мы будем использовать AnyDecodable для этого свойства

    enum CodingKeys: String, CodingKey {
        case id, email, metadata
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(String.self, forKey: .id)
        email = try container.decode(String.self, forKey: .email)

        let metadataContainer: AnyDecodable = try container.decode(AnyDecodable.self, forKey: .metadata)
        guard let metadata = metadataContainer.value as? [String: Any] else {
            throw DecodingError.typeMismatch([String: Any].self, DecodingError.Context(codingPath: [CodingKeys.metadata], debugDescription: "Expected '[String: Any]' for 'metadata' key"))
        }
        self.metadata = metadata
    }
}

Шаг 4: Использование JSONDecoder

После определения структуры мы можем использовать JSONDecoder для декодирования нашего JSON:

let jsonData = """
{
  "id": "4yq6txdpfadhbaqnwp3",
  "email": "[email protected]",
  "metadata": {
    "link_id": "linked-id",
    "buy_count": 4
  }
}
""".data(using: .utf8)!

do {
    let customer = try JSONDecoder().decode(Customer.self, from: jsonData)
    print(customer)
} catch {
    print("Ошибка декодирования: \(error)")
}

Заключение

Теперь вы можете эффективно декодировать произвольные JSON-словарные структуры в вашем Swift-коде. Этот подход предоставляет возможность работы с динамическими JSON-структурами, что важно в эру, где API часто возвращают разные данные. Благодаря созданию универсального типа AnyDecodable, вы можете легко обрабатывать данные, не зная их заранее.

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

Этот подход не только универсален, но и расширяем, что позволит легко адаптироваться к изменениям в спецификациях API в будущем.

Оцените материал
Добавить комментарий

Капча загружается...