В своём развитии мне пришлось пробежаться по нескольким этапам в нескольких направлениях: Java → Objective C → Swift, Web → Enterprise → Mobile, XML → JSON. Этим путём я шёл на протяжении более 15 лет, подолгу и внимательно задерживаясь на каждом этапе. Нужно идти дальше. За мобильными приложениями можно придумать что-то (наверное, пока не хочется), языков вообще пруд-пруди, ничего интереснее JSON'а не придумали. А зачем его менять?
Дальше я расскажу почему мне не очень нравится JSON для некоторых применений и как, по моему мнению, его можно доработать, чтобы стало немного удобнее.
Сразу должен отметить, что не рассматриваю KTV, как замену JSON. И ни в коем случае не рассматриваю его для использования в JavaScript'е. Это будет неудобно и неправильно. С другой стороны, ситуация, когда система описания объектов JavaScript'а используется в других языках для работы с типизированными данными — тоже странная, и её хочется поправить.
Плюсы и минусы JSON
Сам по себе JSON хороший.
- Простой. Отдельное спасибо за отсутствие комментариев. Если бы их оставили, туда начали бы пихать мета-информацию (потому что JSON бедный очень) и нотация быстро превратилась бы в помойку.
- Выразительный. Не так, как XML, но достаточно для широкого круга задач.
- Сильно короче в записи, чем XML.
Болезни JSON'а растут из истории создания. Нужно было, чтобы eval(что-то)
в JavaScript'е выдавал объект, с которым удобно работать. К сожалению, eval'ы в других языках работают обычно по-другому (если вообще присутствуют), и сами языки отличаются. Используя этот формат с не-JavaScript, видны его недостатки.
- Кавычки. По стандарту (в качестве стандарта я беру текст отсюда: http://www.json.org) все названия должны заключаться в кавычки. Если мы используем JSON, например, для передачи данных по сети, то это излишество.
- Отсутствие типизации. Точнее, типы есть, но всего строка/число/true/false/null. И объекты с массивами. Всё. Ни дат, ни целых/дробных чисел нет. Нет возможности пометить типами объекты, чтобы проще было модель понять.
- Отсутствие стандартов записи объектов. Это, например, может привести к массивам со смешанными объектами внутри. Когда приходится такое парсить — становится больно.
- Отсутствие ссылок. Если в JSON записывать иерархическую структуру объектов, то регулярно встречаются ссылки на один и тот же объект из разных мест. JSON не даёт возможности сослаться на первое использование. Нужно либо что-то придумывать, либо повторять объекты целиком, что плохо сказывается на размере структуры и на сложности парсинга.
Я попробовал найти формат, который бы работал как JSON (в идеале — являлся бы его надмножеством), но при этом умел стандартным образом решать часть или все перечисленные выше проблемы. Нашёл YAML.
YAML
YAML хорош. Он почти надмножество JSON, хорошо определён (http://www.yaml.org/spec/1.2/spec.html), легко читается. Но вместе с тем:
- вложенность структур определяется отступами. В случае с передачей информации удобна была бы возможность их убирать. Любителей табуляции также не уважают, отступы делаются только пробелами.
- YAML излишне сложный. Большое количество специальных символов, договоренности о структуре делают поддержку YAML'а сложной, парсеры — сложными (YAML 1.2 появился уже 7 лет как, но, согласно новостям с сайта, парсеры до сих пор поддерживают в основном предыдущие версии).
В общем, YAML «не пошёл». Кроме него популярных форматов я не знаю. Если вдруг вы знаете — пишите, с удовольствием посмотрю.
Пришло время представить то, что получилось у меня.
KTV
Key-Type-Value. Я так называю этот формат.
Напишем простой объект на JSON'е:
{
"name": "Alex",
"coolness": 3.1415,
"isAProgrammer": true
}
Если переписать это же на KTV, получится:
{
name: Alex
coolness: 3.1415
isAProgrammer: true
}
Казалось бы, ничего не поменялось. Но давайте присмотримся:
- убрались кавычки. Они теперь обязательны только, если в ключе или значении есть нетривиальная строка. Обычные ситуации кавычек не требуют.
- убрались запятые в конце строк. Их (или точки с запятой) можно ставить, если пишем несколько определений в одну строку. Но в традиционной записи их можно опустить.
И первый и второй примеры — оба являются корректными KTV-файлами. Это очень важно, так как позволяет в KTV-парсер запихнуть стандартный JSON, и он его съест с удовольствием. А можно воспользоваться удобствами, типами например.
Где же тут типы? В вышеприведенном случае типы выводятся из значений. «Alex» — это строчный литерал, значит тип name'а — «string», coolness'у достался «double», а isAProgrammer — «bool». Кроме этих типов есть ещё «null» (он же «nil», привет коллегам), «int» и «color». Давайте запишем этот же пример, но полностью указывая типы:
{
name(string): Alex
coolness(double): 3.1415
isAProgrammer(bool): true
}
Никуда не делись и вложенные объекты/массивы. С дополнениями:
- И у объектов и у массивов могут быть типы. Для массива тип — это общий тип каждого объекта в массиве. Нельзя положить два разных типа (строка/число, или разные объекты) в массив.
- Тип массива или объекта также может указывать на класс, к которому этот объект или массив необходимо привести при парсинге. Например:
property(font): {name:..., size:...}
может обозначать объект-шрифт, а
property(point): [2, 3]
может при распознавании быть преобразовано в объект-точку. В принципе, точку можно сделать также объектом (с пропертями x, y), но зачем? Мы ведь и так знаем, что там где.
Присутствуют сылки, возможность сослаться на любую другую пропертю. Как-то так:
{
property: value
another: @property
}
Это позволяет сократить запись некоторых ветвистых объектов, или просто задавать константы, обращаясь к ним в других частях файла. Первая возможность полезна в REST'е, если структура объектов нетривиальная. Вторая — в конфигурационных файлах.
Идея миксинов также позаимствована из других языков. Они позволяют проперти одного объекта (или объектов) приделать другому объекту. Можно представить себе это, как наследование, но наследование — это из ООП, там поведение наследуется, тут только свойства. Так что лучше смешивать.
{
object: { a: ..., b: ...}
object2(+object): { c: ..., d: ... }
}
В данном примере объект object2 будет после парсинга содержать как свои проперти, так и проперти из объекта object. То есть, a, b, c и d.
Комментарии
Я не удержался и организовал комментарии. Их можно задавать либо при помощи //, либо #. Оба типа комментариев — строчные, блочных комментариев нет.
Со вторым типом (который октоторповый) есть небольшая проблема. Дело в том, что литерал цвета предполагается задавать как-раз в стандартном CSS-виде, то есть #rrggbbaa, что конфликтует с текущим вариантом записи комментариев. Так что, возможно, в будущем останется только С-вариант (//).
Работа с форматом
Для меня этот формат, и парсер этого формата нужен в двух местах:
- чтобы хранить стилевую информацию в S2 (развитие Ångström Style System)
- чтобы можно было улучшить общение между своими собственными серверами на Swift и приложениями на нём же.
Поэтому я рассматриваю работу с форматом именно для Swift'а. В принципе, формат достаточно простой и не должно быть сложно организовать его парсер в любом языке.
Проблемы Swift сейчас не позволяют сделать по-настоящему правильную загрузку/сериализацию объектов из/в KTV. Поэтому процесс работы с форматом предполагается следующий:
- Создаются модельные классы. Как обычные классы (в том числе аннотированные
@objc
) или структуры. Каждый класс должен реализовывать протоколKTVGenerated
. - При помощи генератора создаются расширения к этим классам, которые умеют загружать и сериализовывать объекты.
- Используем сгенерированное для преобразования KTV в объекты и обратно (или в/из JSON).
Модельная структура может выглядеть, например, так:
public struct RootObject: KTVGenerated {
var string:String // просто пропертя
var stringOrNil:String? // optional
var int:Int // числа
var stringArray:[String] // массивы
var stringDictionary:[String:Int] // ассоциативные массивы
var object:ChildObject? // ссылка на другой модельный класс
private var _privateString:String // эта пропертя не будет сериализовываться
private(set) var _privateSetString:String // эта — тоже
let _constantString:String // и константы тоже не сериализуем
}
Видно, что поддерживаются все основные фичи:
- Типы (они должны прописываться явно, чтобы генератор смог корректно создать расширения классов.
- Коллекции (массивы и ассоциативные массивы).
- Иерархии объектов (ссылки на другие модельные классы)
- Проперти, которые не участвуют в сериализации (это константы, приватные проперти и проперти с приватными сеттерами)
История создания такого парсера — тема для отдельной статьи. Задачи одновременного поддержания структур и классов, способы задания атрибутов без наличия самих атрибутов, использование SourceKit'а вместо рефлекшна для анализа файлов, необходимость кастомных мапперов (например, для парсинга нестандартных дат). Сам парсер сейчас в процессе активной разработки, и постоянно меняется. Но, если будет достаточный интерес — выложу рабочий проект, обсудим варианты.
Польза от очередного формата данных
https://xkcd.com/927/
JSON — так себе формат для передачи данных между приложениями. Это — формат описания объектов в JavaScript'е. А мы постоянно используем его для совершенно других целей, впихивая и по делу и без дела.
Мне кажется, пора переходить к чему-то, более приспособленному к выполняемым задачам. К новому Swift'у с его строгой типизацией, к сложным структурам данных, которые часто приходится передавать на мобильные клиенты с сервера. Нужен формат, синтаксис которого можно будет проверять в IDE, в процессе компиляции и при парсинге. И, мне кажется, что KTV, не меняя кардинально структуру, практически не усложняя уже известный формат, добавляет несколько удобных мелочей.
Скорее всего, я много чего упустил или не учёл. Может, у кого-то есть похожий опыт? Пишите в комментариях, или в почту: alex@jdnevnik.com, обсудим!
Автор: bealex