Как декодировать свойство с типом словаря 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 54

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)