Вопрос или проблема
Предположим, у меня есть 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:
пример кода:
Самый простой и рекомендуемый способ – это создать отдельную модель для каждого словаря или модели, которая находится в 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 в будущем.