Как декодировать свойство с типом словаря JSON в протоколе Swift 4 decodable
Допустим, у меня есть Customer
тип данных, который содержит metadata
свойство, которое может содержать любой словарь JSON в объекте customer
struct Customer {
let id: String
let email: String
let metadata: [String: Any]
}
{
"object": "customer",
"id": "4yq6txdpfadhbaqnwp3",
"email": "john.doe@example.com",
"metadata": {
"link_id": "linked-id",
"buy_count": 4
}
}
The metadata
свойство может быть любым произвольным объектом карты JSON.
прежде чем я смогу привести свойство из десериализованного JSON из NSJSONDeserialization
но с новым Swift 4 Decodable
протокол, я все еще не могу придумать способ сделать это.
а кто-нибудь знает, как достичь этого в Swift 4 с Декодируемым протоколом?
10 ответов:
С некоторым вдохновением от в этом суть я нашел, я написал некоторые расширения для
UnkeyedDecodingContainer
иKeyedDecodingContainer
. Вы можете найти ссылку на мой gist здесь. С помощью этого кода теперь вы можете декодировать любой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)
Edit: есть один нюанс, который я нашел, который декодирует массив словарей
[[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 { // appropriate error handling return }
расширения
// Inspired by 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 { // See if the current value in the JSON array is `null` first and prevent infite recursion with nested arrays. 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) } }
Я тоже играл с этой проблемой и, наконец, написал простая библиотека для работы с типами "generic 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
.
когда я нашел старый ответ, я проверил только простой случай объекта JSON, но не пустой, который вызовет исключение времени выполнения, такое как @slurmomatic и @zoul found. Извините за эту проблему.
поэтому я пытаюсь по-другому, имея простой протокол JSONValue, реализовать
AnyJSONValue
введите erasure struct и используйте этот тип вместо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: "Unsupported JSON tyep")) } } }
а вот как его использовать при декодировании
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)
в инициализаторе.
мне было плохо пропустить это в первую очередь.
Я пришел с немного другим решением.
предположим, у нас есть что-то большее, чем просто
[String: 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: "the container contains nothing serialisable") } } else { throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Could not serialise")) } } }
попробуйте использовать
let stud = try! JSONDecoder().decode(AnyDecodable.self, from: jsonData).value as! [String: Any] print(stud)
вы можете создать структуру метаданных, которая подтверждает
Codable
протокол и использоватьDecodable
класс для создания объекта, как показано нижеlet json: [String: Any] = [ "object": "customer", "id": "4yq6txdpfadhbaqnwp3", "email": "john.doe@example.com", "metadata": [ "link_id": "linked-id", "buy_count": 4 ] ] struct Customer: Codable { let object: String let id: String let email: String let metadata: Metadata } struct Metadata: Codable { 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) }
вы могли бы взглянуть на BeyovaJSON
import BeyovaJSON struct Customer: Codable { let id: String let email: String let metadata: JToken } //create a customer instance customer.metadata = ["link_id": "linked-id","buy_count": 4] let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted print(String(bytes: try! encoder.encode(customer), encoding: .utf8)!)
самый простой и предлагаемый способ-это создать отдельную модель для каждого словаря или модели, которая находится в JSON.
вот что я делаю
//Model for dictionary **Metadata** struct Metadata: Codable { var link_id: String? var buy_count: Int? } //Model for dictionary **Customer** struct Customer: Codable { var object: String? var id: String? var email: String? var metadata: Metadata? } //Here is our decodable parser that decodes JSON into expected model struct CustomerParser { var customer: Customer? } extension CustomerParser: Decodable { //keys that matches exactly with 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) // defining our (keyed) container let object: String = try container.decode(String.self, forKey: .object) // extracting the data let id: String = try container.decode(String.self, forKey: .id) // extracting the data let email: String = try container.decode(String.self, forKey: .email) // extracting the data //Here I have used metadata model instead of dictionary [String: Any] let metadata: Metadata = try container.decode(Metadata.self, forKey: .metadata) // extracting the data 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 { } }
**я использовал опционально, чтобы быть в безопасной стороне при разборе, может быть изменен по мере необходимости.
вот более общий (не только
[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: "Expected '[String: Any]' for 'metadata' key") 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) { // Wrapping numeric and boolean types in `NSNumber` is important, so `as? Int64` or `as? Float` casts will work 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 { // NOP } 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: "Unexpected value for \(key.stringValue) key") } } 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: "Unexpected value") } } else { let context = DecodingError.Context(codingPath: [], debugDescription: "Invalid data format for 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 { // as it always will be nil
Если вы используете SwiftyJSON для разбора JSON, вы можете обновить до 4.1.0 имеющего
Codable
поддержка протокола. Просто объявитеmetadata: JSON
и все готово.import SwiftyJSON struct Customer { let id: String let email: String let metadata: JSON }
то, что вы хотите пойти против дизайна
Codable
. Идея заCodable
должен обеспечить механизм для архивирования и разархивирования данных безопасным для типа способом. Это означает, что вы должны определить свойства и их типы данных перед рукой. Я могу придумать 2 решения вашей проблемы:1. Перечислите все потенциальные ключи метаданных
часто, если вы достаточно глубоко проникнете в документацию API, вы найдете полный список всех потенциальных ключей метаданных. Определить
Metadata
struct, с этими ключами в качестве необязательных свойств:struct Customer: Decodable { struct Metadata: Decodable { var linkId: String? var buyCount: Int? var somethingElse: Int? private enum CodingKeys: String, CodingKey { case linkId = "link_id" case buyCount = "buy_count" case somethingElse = "something_else" } } var object: String var id: String var email: String var metadata: Metadata } let customer = try! JSONDecoder().decode(Customer.self, from: jsonData) print(customer.metadata)
Я вижу, что разработчики Swift предпочли бы такой подход.
2. Объединить Декодируемый и JSONSerialization
JSONSerialization
предлагает большой динамизм в компромиссе для типа безопасности. Вы можете определенно смешать его сDecodable
, чья философия дизайна прямо противоположная:struct Customer { private struct RawCustomer: Decodable { var object: String var id: String var email: String } var object: String var id: String var email: String var metadata: [String: AnyObject] init(jsonData: Data) throws { let rawCustomer = try JSONDecoder().decode(RawCustomer.self, from: jsonData) object = rawCustomer.object id = rawCustomer.id email = rawCustomer.email let jsonObject = try JSONSerialization.jsonObject(with: jsonData) if let dict = jsonObject as? [String: AnyObject], let metadata = dict["metadata"] as? [String: AnyObject] { self.metadata = metadata } else { self.metadata = [String: AnyObject]() } } } let customer = try! Customer(jsonData: jsonData) print(customer.metadata)