Swift Codable: как кодировать данные верхнего уровня во вложенный контейнер - PullRequest
0 голосов
/ 22 мая 2018

Мое приложение использует сервер, который возвращает JSON, который выглядит следующим образом:

{
    "result":"OK",
    "data":{

        // Common to all URLs
        "user": {
            "name":"John Smith" // ETC...
        },

        // Different for each URL
        "data_for_this_url":0
    }
}

Как видите, информация, относящаяся к URL, существует в том же словаре, что и общий user словарь.

ЦЕЛЬ:

  1. Расшифруйте этот JSON в классы / структуры.
    • Поскольку user встречается часто, я хочу, чтобы это было в классе / структуре верхнего уровня.
  2. Кодирование в новый формат (например, plist).
    • Мне нужно сохранить первоначальную структуру.(т.е. воссоздать словарь data из информации верхнего уровня user и информации дочернего объекта)

ПРОБЛЕМА:

При перекодировании данных я не могу записать словарь user (из объекта верхнего уровня) и данные, относящиеся к URL (из дочернего объекта), в кодировщик.

Любой из них user перезаписывает другойданные или другие данные перезаписывают user.Я не знаю, как их объединить.

Вот что у меня есть:

// MARK: - Common User
struct User: Codable {
    var name: String?
}

// MARK: - Abstract Response
struct ApiResponse<DataType: Codable>: Codable {
    // MARK: Properties
    var result: String
    var user: User?
    var data: DataType?

    // MARK: Coding Keys
    enum CodingKeys: String, CodingKey {
        case result, data
    }
    enum DataDictKeys: String, CodingKey {
        case user
    }

    // MARK: Decodable
    init(from decoder: Decoder) throws {
        let baseContainer = try decoder.container(keyedBy: CodingKeys.self)
        self.result = try baseContainer.decode(String.self, forKey: .result)
        self.data = try baseContainer.decodeIfPresent(DataType.self, forKey: .data)

        let dataContainer = try baseContainer.nestedContainer(keyedBy: DataDictKeys.self, forKey: .data)
        self.user = try dataContainer.decodeIfPresent(User.self, forKey: .user)
    }

    // MARK: Encodable
    func encode(to encoder: Encoder) throws {
        var baseContainer = encoder.container(keyedBy: CodingKeys.self)
        try baseContainer.encode(self.result, forKey: .result)

        // MARK: - PROBLEM!!

        // This is overwritten
        try baseContainer.encodeIfPresent(self.data, forKey: .data)

        // This overwrites the previous statement
        var dataContainer = baseContainer.nestedContainer(keyedBy: DataDictKeys.self, forKey: .data)
        try dataContainer.encodeIfPresent(self.user, forKey: .user)
    }
}

ПРИМЕР:

В приведенном ниже примереперекодированный plist не включает order_count, потому что он был перезаписан словарем, содержащим user.

// MARK: - Concrete Response
typealias OrderDataResponse = ApiResponse<OrderData>

struct OrderData: Codable {
    var orderCount: Int = 0
    enum CodingKeys: String, CodingKey {
        case orderCount = "order_count"
    }
}


let orderDataResponseJson = """
{
    "result":"OK",
    "data":{
        "user":{
            "name":"John"
        },
        "order_count":10
    }
}
"""

// MARK: - Decode from JSON
let jsonData = orderDataResponseJson.data(using: .utf8)!
let response = try JSONDecoder().decode(OrderDataResponse.self, from: jsonData)

// MARK: - Encode to PropertyList
let plistEncoder = PropertyListEncoder()
plistEncoder.outputFormat = .xml

let plistData = try plistEncoder.encode(response)
let plistString = String(data: plistData, encoding: .utf8)!

print(plistString)

// 'order_count' is not included in 'data'!

/*
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>data</key>
    <dict>
        <key>user</key>
        <dict>
            <key>name</key>
            <string>John</string>
        </dict>
    </dict>
    <key>result</key>
    <string>OK</string>
</dict>
</plist>
*/

Ответы [ 2 ]

0 голосов
/ 27 мая 2018

Отличный вопрос и решение, но если вы хотите упростить его, вы можете использовать KeyedCodable , который я написал.Вся реализация вашего Codable будет выглядеть так (OrderData и User остаются неизменными, конечно):

struct ApiResponse<DataType: Codable>: Codable, Keyedable {
  // MARK: Properties
  var result: String!
  var user: User?
  var data: DataType?

  mutating func map(map: KeyMap) throws {
      try result <-> map["result"]
      try user <-> map["data.user"]
      try data <-> map["data"]
  }

  init(from decoder: Decoder) throws {
      try KeyedDecoder(with: decoder).decode(to: &self)
 }

}

0 голосов
/ 24 мая 2018

У меня только что было прозрение, когда я просматривал протоколы кодировщика.

KeyedEncodingContainerProtocol.superEncoder(forKey:) метод предназначен именно для этого типа ситуации.

Этот метод возвращает отдельную Encoder, которая можетсобрать несколько элементов и / или вложенных контейнеров, а затем закодировать их в один ключ.

Для этого конкретного случая данные верхнего уровня user можно кодировать, просто вызывая свой собственный метод encode(to:) сновый superEncoder.Затем с помощью кодировщика можно также создать вложенные контейнеры, которые будут использоваться в обычном режиме.

Решение вопроса

// MARK: - Encodable
func encode(to encoder: Encoder) throws {

    var baseContainer = encoder.container(keyedBy: CodingKeys.self)
    try baseContainer.encode(self.result, forKey: .result)

    // MARK: - PROBLEM!!
//    // This is overwritten
//    try baseContainer.encodeIfPresent(self.data, forKey: .data)
//
//    // This overwrites the previous statement
//    var dataContainer = baseContainer.nestedContainer(keyedBy: DataDictKeys.self, forKey: .data)
//    try dataContainer.encodeIfPresent(self.user, forKey: .user)

    // MARK: - Solution
    // Create a new Encoder instance to combine data from separate sources.
    let dataEncoder = baseContainer.superEncoder(forKey: .data)

    // Use the Encoder directly:
    try self.data?.encode(to: dataEncoder)

    // Create containers for manually encoding, as usual:
    var userContainer = dataEncoder.container(keyedBy: DataDictKeys.self)
    try userContainer.encodeIfPresent(self.user, forKey: .user)
}

Вывод:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>data</key>
    <dict>
        <key>order_count</key>
        <integer>10</integer>
        <key>user</key>
        <dict>
            <key>name</key>
            <string>John</string>
        </dict>
    </dict>
    <key>result</key>
    <string>OK</string>
</dict>
</plist>
...