Как декодировать объект JSON с любым количеством пар ключ-значение в Swift? - PullRequest
0 голосов
/ 14 июля 2020

Я использую Swift, чтобы попытаться декодировать JSON: API -форматированные JSON результаты. JSON, который я пытаюсь проанализировать, имеет такую ​​форму:

{
    "data": {
        "type": "video",
        "id": "1",
        "attributes": {
            "name": "Test Video",
            "duration": 1234
        }
    }
}

Я пытаюсь создать структуру Swift, которая будет кодировать эти JSON объекты, но у меня возникают проблемы с ключ attributes, так как он может содержать любое количество атрибутов.

Структуры Swift, которые я пытаюсь закодировать, выглядят так:

struct JSONAPIMultipleResourceResponse: Decodable {
    var data: [JSONAPIResource]
}
struct JSONAPIResource: Decodable {
    var type: String
    var id: String
    var attributes: [String, String]?
}

* Атрибуты 1013 * и id должны присутствовать в каждый JSON: результат API. Ключ attributes должен быть списком любого количества пар ключ-значение; и ключ, и значение должны быть строками.

Затем я пытаюсь декодировать ответ JSON от API следующим образом:

let response = try! JSONDecoder().decode(JSONAPIMultipleResourceResponse.self, from: data!)

Вышеупомянутое работает, если я оставлю * Свойства 1023 * и id в моей структуре JSONAPIResource Swift, но как только я пытаюсь сделать что-нибудь с attributes, я получаю следующую ошибку:

Fatal error: 'try!' expression unexpectedly raised an error: Swift.DecodingError.valueNotFound(Swift.String, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "data", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0), CodingKeys(stringValue: "attributes", intValue: nil), _JSONKey(stringValue: "poster_path", intValue: nil)], debugDescription: "Expected String but found null value instead.", underlyingError: nil)): file /Users/[User]/Developer/[App]/LatestVideosQuery.swift, line 35  
2020-07-14 16:13:08.083721+0100 [App][57157:6135473] Fatal error: 'try!' expression unexpectedly raised an error: Swift.DecodingError.valueNotFound(Swift.String, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "data", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0), CodingKeys(stringValue: "attributes", intValue: nil), _JSONKey(stringValue: "poster_path", intValue: nil)], debugDescription: "Expected String but found null value instead.", underlyingError: nil)): file /Users/[User]/Developer/[App]/LatestVideosQuery.swift, line 35

Я получаю, что Swift очень строго типизирован , но я не уверен, как кодировать эти неструктурированные данные в Swift. Мой план состоит в том, чтобы иметь общие c JSONAPIResource представления ресурсов, возвращаемых из моего API, а затем я могу сопоставить их со структурами модели в моем приложении Swift, т.е. преобразовать указанные выше ресурсы JSON: API в структуру Video. .

Кроме того, я пытаюсь наивно преобразовать значения в объекте attributes в строки, но, как вы можете видеть, duration - это целочисленное значение. Если есть способ, чтобы attributes в моей структуре JSONAPIResource сохранял примитивные значения, такие как целые числа и логические значения, а не только строки, я бы хотел прочитать, как!

Ответы [ 2 ]

1 голос
/ 14 июля 2020

Если это полностью общий c пакет ключей / значений (который может указывать на необходимость возможного изменения дизайна), вы можете создать enum для хранения различных (примитивных) значений, которые может содержать JSON. :

enum JSONValue: Decodable {
   case number(Double)
   case integer(Int)
   case string(String)
   case bool(Bool)
   case null

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

      if let int = try? container.decode(Int.self) {
         self = .integer(int)
      } else if let double = try? container.decode(Double.self) {
         self = .number(double)
      } else if let string = try? container.decode(String.self) {
         self = .string(string)
      } else if let bool = try? container.decode(Bool.self) {
         self = .bool(bool)
      } else if container.decodeNil() {
         self = .null
      } else {
        // throw some DecodingError
      }
   }
}

, а затем вы можете установить attributes на:

var attributes: [String: JSONValue]
0 голосов
/ 14 июля 2020

Если существует согласованная связь между значением type и парами ключ-значение в attributes, я рекомендую объявить attributes как перечисление со связанными типами и декодировать тип в зависимости от значения type.

Например

let jsonString = """
{
    "data": {
        "type": "video",
        "id": "1",
        "attributes": {
            "name": "Test Video",
            "duration": 1234
        }
    }
}
"""

enum ResourceType : String, Decodable {
   case video
}

enum AttributeType {
    case video(Video)
}

struct Video : Decodable {
    let name : String
    let duration : Int
}

struct Root : Decodable {
    let data : Resource
}

struct Resource : Decodable {
    let type : ResourceType
    let id : String
    let attributes : AttributeType
    
    private enum CodingKeys : String, CodingKey { case type, id, attributes }
    
    init(from decoder : Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.type = try container.decode(ResourceType.self, forKey: .type)
        self.id = try container.decode(String.self, forKey: .id)
        switch self.type {
            case .video:
                let videoAttributes = try container.decode(Video.self, forKey: .attributes)
                attributes = .video(videoAttributes)
        }
    }
}

let data = Data(jsonString.utf8)

do {
    let result = try JSONDecoder().decode(Root.self, from: data)
    print(result)
} catch {
    print(error)
}
...