Возможно ли, чтобы строка JSON с другим именем поля была декодирована в один и тот же объект структуры?

#ios #json #swift

Вопрос:

В настоящее время у меня есть следующие 2 строки JSON — unoptimized_json и optimized_json .

 let unoptimized_json = "[{"id":1,"text":"hello","checked":true}]"

let optimized_json = "[{"i":1,"t":"hello","c":true}]"
 

Я хотел бы декодировать их в один и тот же объект структуры.

 struct Checklist: Hashable, Codable {
    let id: Int64
    var text: String?
    var checked: Bool
    
    enum CodingKeys: String, CodingKey {
        case id = "i"
        case text = "t"
        case checked = "c"
    }
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
    
    static func == (lhs: Checklist, rhs: Checklist) -> Bool {
        return lhs.id == rhs.id
    }
}
 

Однако текущий дизайн может принимать только формат optimized_json , а не unoptimized_json .

В Java Android я могу добиться этого с помощью alternate .

 import com.google.gson.annotations.SerializedName;

public class Checklist {

    @SerializedName(value="i", alternate="id")
    private final long id;

    @SerializedName(value="t", alternate="text")
    private String text;

    @SerializedName(value="c", alternate="checked")
    private boolean checked;
}
 

Мне было интересно, в Swift есть ли у нас эквивалентная функция для достижения этой цели?

Возможно ли, чтобы строка JSON с другим именем поля была декодирована в один и тот же объект структуры?

Комментарии:

1. Знаете ли вы, какой из них вы получаете раньше, например, другую конечную точку или по какому-либо параметру в запросе?

2. Не совсем, так как данные генерируются сторонним приложением. Это может быть любой из форматов.

3. Это оптимизация по пирру . Усилия по обработке различных форматов намного больше, чем польза от нескольких сохраненных символов.

4. @vadian в зависимости от загрузки сервера любое сокращение объема отправляемых данных может оказать положительное влияние. Мы говорим здесь не только о фактической длине json, но также о форматировании json и других процессах, таких как сжатие полезной нагрузки, которые складываются.

Ответ №1:

Невозможно сделать это автоматически, но вы можете реализовать свою собственную логику декодирования и попробовать использовать там разные значения.

 struct User: Decodable {
  enum CodingKeys: String, CodingKey {
    case i, id
  }

  let id: Int

  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    let i = try container.decodeIfPresent(Int.self, forKey: .i)
    let id = try container.decodeIfPresent(Int.self, forKey: .id)
    guard let identifier = i ?? id else { throw NoIDFoundError }
    self.id = identifier
  }
}
 

Ответ №2:

Я бы предложил использовать обычай keyDecodingStrategy при декодировании и позволить этой стратегии возвращать правильный ключ. Это решение предполагает, что имена оптимизированных ключей всегда являются первой буквой(буквами) обычного ключа. Его можно использовать и в других случаях, но тогда требуется более жестко запрограммированное решение.

Сначала мы используем наш собственный тип для ключей, интересная часть заключается в init(stringValue: String) том, где мы используем CodingKeys перечисление (см. Ниже) CheckList , чтобы получить правильный ключ

 struct VaryingKey: CodingKey {
    var intValue: Int?

    init?(intValue: Int) {
        self.intValue = intValue
        stringValue = "(intValue)"
    }

    var stringValue: String

    init(stringValue: String) {
        self.stringValue = Checklist.CodingKeys.allCases
            .first(where: { stringValue == $0.stringValue.prefix(stringValue.count) })?
            .stringValue ?? stringValue
    }
}
 

Нам нужно определить CodingKeys перечисление для контрольного списка и привести его в соответствие с CaseIterable приведенным выше кодом для работы

 enum CodingKeys: String, CodingKey, CaseIterable {
    case id
    case text
    case type
    case checked
}
 

и затем мы используем это при декодировании

 let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .custom({ codingPath in
    let key = codingPath.last!.stringValue
    return VaryingKey(stringValue: key)
})

do {
    let unoptimized = try decoder.decode([Checklist].self, from: unoptimized_json.data(using: .utf8)!)
    let optimized = try decoder.decode([Checklist].self, from: optimized_json.data(using: .utf8)!)
    print(unoptimized)
    print(optimized)
} catch {
    print(error)
}
 

[__lldb_expr_377.Контрольный список(идентификатор: 1, текст: Необязательно(«привет»), тип: «мир», проверено: верно)]
[__lldb_expr_377.Контрольный список(идентификатор: 1, текст: Необязательно(«привет»), тип: «мир», проверено: верно)]

(Дополнительный атрибут «тип» использовался только для тестирования решения)


Обновить

Я упоминал выше, что если короткая версия ключа json не могла быть динамически вычтена из обычного ключа, то требовалось более жестко закодированное решение, поэтому для полноты картины вот пример такого решения с новым init(stringValue:) в VaryingKey

 init(stringValue: String) {
    if let codingKey = Checklist.CodingKeys(stringValue: stringValue) {
        print(codingKey)
        self.stringValue = stringValue
    } else {
        switch stringValue {
        case "i":
            self.stringValue = Checklist.CodingKeys.id.stringValue
        case "t":
            self.stringValue = Checklist.CodingKeys.text.stringValue
        case "ty":
            self.stringValue = Checklist.CodingKeys.type.stringValue
        case "c":
            self.stringValue = Checklist.CodingKeys.checked.stringValue
        default:
            fatalError()
        }
    }
}