На данный момент подавляющее большинство мобильных приложений являются клиент-серверными. Повсюду происходит подгрузка, синхронизация, отправка событий и основным способом взаимодействия с сервером является обмен данными посредством формата json.


Key decoding


В Foundation присутствует два механизма сериализации-десереадизации данных. Старый — NSJsonSerialization и новый – Codable. Последний в списке достоинств имеет в себе такую замечательную вещь, как автогенерация ключей для json данных на основе структуры (или класса), реализующей Codable (Encodable, Decodable) и инициализатора для декодирования данных.


И вроде бы всё прекрасно, можно использовать и радоваться, но реальность не так проста.
Довольно часто на сервере можно встретить json вида:


{"topLevelObject":
  {
    "underlyingObject": 1
  },
  "Error": {
    "ErrorCode": 400,
    "ErrorDescription": "SomeDescription"
  }
}

Это почти реальный пример с одного из серверов по проекту.


Для класса JsonDecoder можно указать работу со snake_case ключами, но что делать, если у нас UpperCamelCase, dash-snake-case или вообще сборная солянка, а вручную ключи писать не хочется?


К счастью, Apple предоставила возможность конфигурировать преобразование ключей перед их сопоставлением с CodingKeys структуры с помощью JSONDecoder.KeyDecodingStrategy. Этим мы и воспользуемся.


Для начала создадим структуру, реализующую протокол CodingKey, потому что таковой нет в стандартной библиотеке:


  struct AnyCodingKey: CodingKey {

    var stringValue: String
    var intValue: Int?

    init(_ base: CodingKey) {
      self.init(stringValue: base.stringValue, intValue: base.intValue)
    }

    init(stringValue: String) {
      self.stringValue = stringValue
    }

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

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

Затем необходимо отдельно обработать каждый case наших ключей. Основные:
snake_case, dash-snake-case, lowerCamelCase и UpperCamelCase. Проверяем, запускаем, всё работает.


Затем сталкиваемся с довольно ожидаемой проблемой: аббревиатуры в camelCase’ах (вспомните многочисленные id, Id, ID). Чтобы всё заработало, необходимо правильно их преобразовать и ввести правило – аббревиатуры преобразуются в camelCase, сохраняя большой только первую букву и myABBRKey превратится в myAbbrKey.


Данное решение отлично работает и для сочетаний нескольких кейсов.


Note: Implementation will be provided into .custom key decoding strategy.


static func convertToProperLowerCamelCase(keys: [CodingKey]) -> CodingKey {
  guard let last = keys.last else {
    assertionFailure()
    return AnyCodingKey(stringValue: "")
  }
  if let fromUpper = convertFromUpperCamelCase(initial: last.stringValue) {
    return AnyCodingKey(stringValue: fromUpper)
  } else if let fromSnake = convertFromSnakeCase(initial: last.stringValue) {
    return AnyCodingKey(stringValue: fromSnake)
  } else {
    return AnyCodingKey(last)
  }
}

Date decoding


Следующая рутинная проблема – способ передачи дат. Микросервисов на сервере много, команд чуть меньше, но тоже приличное количество и в итоге мы оказываемся перед кучей форматов дат вида «да я стандартную использую». К тому же, кто-то передаёт даты строкой, кто-то в Epoch-time. В итоге у нас снова оказывается сборная солянка из сочетаний строки-числа-таймзоны-миллисекунд-разделителей, а DateDecoder в iOS жалуется и требует строгий формат дат. Решение тут несложное, просто перебором ищем признаки того или иного формата и комбинируем их, получая в итоге необходимый. Данные форматы успешно и стопроцентно покрывали мои кейсы.


Note: This is custom DateFormatter initializer. Its just set format to created formatter.


static let onlyDate = DateFormatter(format: "yyyy-MM-dd")
static let full = DateFormatter(format: "yyyy-MM-dd'T'HH:mm:ss.SSSx")
static let noWMS = DateFormatter(format: "yyyy-MM-dd'T'HH:mm:ssZ")
static let noWTZ = DateFormatter(format: "yyyy-MM-dd'T'HH:mm:ss.SSS")
static let noWMSnoWTZ = DateFormatter(format: "yyyy-MM-dd'T'HH:mm:ss")

Пристёгиваем это к нашему декодеру с помощью JSONDecoder.DateDecodingStrategy и получаем декодер, который обрабатывает почти всё, что угодно и преобразует в удобоваримый для нас формат.


Тесты производительности


Тесты производились для json строки размером 7944 байта.


convertFromSnakeCase strategy anyCodingKey strategy
Absolute 0.00170 0.00210
Relative 81% 100%

Как мы видим, кастомный Decoder медленее примено на 20% из-за обязательной проверки каждого ключа в json на необходимость трансформирования. Однако, это является небольшой платой за отсутствие необходимости явно прописывать ключи для структур данных, реализуя Codable. Количество боилерплейта очень сильно сократилось в проекте с добавлением данного декодера. Стоит ли его использовать, чтобы сохранить время разработчика, но ухудшить проихводительность? Решать вам.


Полный пример кода в библиотеке на github


Статья на английском

Комментарии (3)


  1. crea7or
    28.04.2019 18:13
    +1

    Похоже на программирование ради программирования.


    1. RomanKerimov
      29.04.2019 15:57

      Да, было бы лучше придерживаться одного из предлагаемых стандартов именования по умолчанию. А ещё лучше было бы использовать единую кодовую базу на клиенте и на сервере для тех данных, что передаются через сеть.


  1. PowerMetall
    29.04.2019 13:42

    О да, с «проблемой формата даты» когда-то сталкивался. Web API, написанное на C# и Windows, разворачивал на Debian.
    Когда в одном окружении случае условный DateTime.Now().ToString() возвращает «dd:mm.yyyy hh:MM:ss», а в другом — «mm/dd/yyyy hh.MM.ss PM».
    Было такое, что оно напрямую отдавалось на клиент в ответе, и тот слегка ломался :)

    Вообще же с тех пор стараюсь дату хранить в БД в формате Unixtime в поле типа INT (зануда: ну хорошо, bigint :) ), и при отдаче конвертировать в то, что ждет клиент.
    А при приеме — имеется давно написанный метод, который «хавает» любой формат на входе.
    Не знаю, правильно это или нет, но по крайней мере больше не боюсь что что-то упадет у меня, или на клиенте.