Обработка HTML-форм в веб-приложениях — несложная задача. Казалось бы, о чем говорить: набросал форму в шаблоне, создал обработчики на сервере, и готово. Проблемы начинаются, когда форма разрастается: нужно следить за полями, их ID, атрибутами name, корректно маппить атрибуты на бэкенде при генерации и процессинге данных. А если часть формы нужно еще и переиспользовать, то разработка превращается в постоянную рутину: приходится бесконечно копировать атрибуты тегов с клиента на сервер и делать копипаст однотипного кода. Однако есть способы сделать работу с формами удобной.
- Зачем это нужно?
- Установка
- Создание формы
- Работа с формой
- Генерация формы ( GET /users/new)
- Парсинг пейлоада ( POST /users)
- Опции для частичного парсинга пейлоада
- Валидаторы
- Динамическое изменение свойств полей формы
- Сборные и наследуемые формы
- Заполнение реляционных полей (one-to-many, many-to-many)
- Кастомные виджеты и расширения
- Вместо заключения
Зачем это нужно?
Чтобы понять, какую проблему мы решаем, давайте взглянем на простой пример. Представьте, что в нашем веб-приложении есть форма для создания пользователей.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<form action=""> <!-- personal info --> <input type="text" id="f_name" name="f_name" placeholder="John" /> <input type="text" id="l_name" name="l_name" placeholder="Dow" /> <!-- account info --> <input type="password" id="password" name="password" placeholder="**********" /> <!-- meta info --> <select name="gender" id="gender"> <option value="0">Male</option> <option value="1" selected>Femate</option> </select> <input type="city" id="city" name="city" placeholder="Saint-Petersburg" /> <textarea name="signature" id="signature" cols="30" rows="10"></textarea> <input type="submit" value="Create user!" /> </form> |
Эта форма выглядит довольно просто. Но использование в реальном приложении добавит ряд задач.
- У каждого поля (или в одном блоке) нужно вывести информацию об ошибках, которые могут появиться при валидации формы.
- Скорее всего, для некоторых полей мы захотим иметь подсказки.
- Наверняка нам нужно будет повесить по одному или несколько CSS-классов на каждое поле, или даже делать это динамически.
- Часть полей должна содержать предзаполненные данные с бэкенда — предыдущие попытки сабмита формы или данные для выпадающих списков. Частный случай с полем gender прост, однако опции для селекта могут формироваться запросами к БД.
И так далее. Все эти доделки раздуют нашу форму как минимум вдвое.
А теперь посмотрим на то, как мы будем обрабатывать эту форму на сервере. Для каждого поля мы должны сделать следующее.
- Корректно смаппить его по name.
- Проверить диапазон допустимых значений — валидировать форму.
- Если были ошибки, сохранить их, вернув форму для редактирования назад на клиентскую часть.
- Если все ОK, то смаппить их на объект БД или аналогичную по свойствам структуру для дальнейшего процессинга.
Вдобавок при создании пользователя вам как админу нужно заполнять только часть данных ( email и password), остальное пользователь заполнит сам в профиле. В этом случае вам, скорее всего, придется скопировать шаблон, удалив часть полей, создать идентичный обработчик формы на сервере или вставлять проверки в текущий для различных вариантов формы. Логику валидации полей придется или копировать, или выносить в отдельную функцию. При этом нужно не запутаться в названиях полей, приходящих с клиента, иначе данные просто потеряются.
Но пользователей нужно не только создавать, но и редактировать, используя ту же самую форму! Причем у админа и юзера эти формы будут разные, с частично пересекающимся набором полей.
Все эти требования резко увеличивают количество шаблонов, обработчиков, валидаторов, которые в лучшем случае будут вынесены в общий модуль, а скорее всего, будут копипаститься по-быстрому. И при необходимости изменить одно поле в форме придется перелопатить все приложение, отлавливая ошибки и опечатки.
Было бы удобнее описать форму в каком-то декларативном формате, например в виде Python-класса, одноразово описав все параметры, классы, валидаторы, обработчики, а заодно предусмотрев возможности ее наследования и расширения. Вот тут-то нам и поможет библиотека WTForms.
Если вы использовали крупные фреймворки типа Django или Rails, вы уже сталкивались со схожей функциональностью в том или ином виде. Однако не для каждой задачи требуется огромный Django. Применять WTForms удобно в паре с легковесными микрофреймворками или в узкоспециализированных приложениях с необходимостью обрабатывать веб-формы, где использование Django неоправданно.
Установка
Начнем с установки самой библиотеки. В моем случае я буду показывать примеры на Python 3. Там, где нужен контекст, код исполняется в обработчике фреймворка aiohttp. Сути это не меняет — примеры будут работать с Flask, Sanic или любым другим модулем. В качестве шаблонизатора используется Jinja2. Устанавливаем через pip:
1 |
pip install wtforms |
Проверяем версию.
1 2 3 |
import wtforms wtforms.__version__ # '2.2.1' |
Попробуем переписать форму выше на WTForms и обработать ее.
Создание формы
В WTForms есть ряд встроенных классов для описания форм и их полей. Определение формы — это класс, наследуемый от встроенного в библиотеку класса Form. Поля формы описываются атрибутами класса, каждому из которых при создании присваивается инстанс класса поля типа, соответствующего типу поля формы. Звучит сложно, на деле проще.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
from wtforms import Form, StringField, TextAreaField, SelectField, validators class UserForm(Form): first_name = StringField('First name', [validators.Length(min=5, max=30)]) last_name = StringField('Last name', [validators.Length(min=5, max=30)]) email = StringField('Email', [validators.Email()]) password = StringField('Password') # meta gender = SelectField('Gender', coerce=int, choices=[ # cast val as int (0, 'Male'), (1, 'Female'), ]) city = StringField('City') signature = TextAreaField('Your signature', [validators.Length(min=10, max=4096)]) |
Вот что мы сделали:
- создали класс UserForm для нашей формы. Он наследован от встроенного Form (и BaseForm);
- каждое из полей формы описали атрибутом класса, присвоив объект встроенного в либу класса типа Field.
В большинстве полей формы мы использовали импортированный класс StringField. Как нетрудно догадаться, поле gender требует ввода другого типа — ограниченного набора значений (м/ж), поэтому мы использовали SelectField. Подпись пользователя тоже лучше принимать не в обычном input, а в textarea, поэтому мы использовали TextAreaField, чье HTML-представление (виджет) — тег <textarea></textarea>. Если бы нам нужно было обрабатывать числовое значение, мы бы импортировали встроенный класс IntegerField и описали бы поле им.
В WTForms множество встроенных классов для описания полей, посмотреть все можно здесь. Также можно создать поле кастомного класса.
О полях нужно знать следующее.
- Каждое поле может принимать набор аргументов, общий для всех типов полей.
- Почти каждое поле имеет HTML-представление, так называемый виджет.
- Для каждого поля можно указать набор валидаторов.
- Некоторые поля могут принимать дополнительные аргументы. Например, для SelectField можно указать набор возможных значений.
- Поля можно добавлять к уже существующим формам. И можно модифицировать, изменять значения на лету. Это особенно полезно, когда нужно чуть изменить поведение формы для одного конкретного случая, при этом не создавать новый класс формы.
- Поля могут провоцировать ошибки валидации по заданным правилам, они будут храниться в form.field.errors.
Работа с формой
Попробуем отобразить форму. Обычный workflow работы с формами состоит из двух этапов.
- GET-запрос страницы, на которой нам нужно отобразить нашу форму. В этот момент мы должны создать инстанс нашей формы, настроить его, если потребуется, и передать шаблонизатору в контексте для рендеринга. Обычно это делается в обработчике (action) контроллера GET-запроса и чем-то похожем в зависимости от HTTP-фреймворка, которым вы пользуетесь (или не пользуетесь, для WTForms это не проблема). Другими словами, в обработчике роута вроде GET /users/new. К слову, в Django или Rails вы выполняете схожие действия. В первом создаете такую же форму и передаете ее шаблонизатору в template context, а во втором создаете в текущем контексте новый, еще не сохраненный объект через метод new ( @user = User.new).
- POST-запрос страницы, с которой мы должны получить данные формы (например, POST /users) и как-то процессить: выполнить валидацию данных, заполнить поля объекта из формы для сохранения в БД.
Генерация формы ([crayon-672b97b8b825d530559270-i/])
Создадим инстанс нашей предварительно определенной формы:
1 2 3 |
user_form = UserForm() type(user_form) # __main__.UserForm |
К каждому полю формы мы можем обратиться отдельно по ее атрибуту:
1 2 |
type(form.first_name) # wtforms.fields.core.StringField |
В самом простом случае это все. Теперь инстанс нашей формы можно передать шаблонизатору для отображения:
1 2 3 4 5 6 |
def new(self, request): user_form = UserForm() render('new_user.html', { 'form': user_form, }) |
Метод render, конечно, специфичен. В вашем случае методы рендеринга будут определяться фреймворком и шаблонизатором, который вы используете.
Отлично, передали нашу форму в шаблонизатор. Как ее отрендерить в шаблоне? Проще простого. Напомню, что мы рассматриваем процесс на примере Jinja2.
1 2 3 4 5 6 7 8 |
{{ form.first_name.label }} {% if form.first_name.errors %} <ul class="errors"> {% for error in form.first_name.errors %} <li>{{ error }}</li>{% endfor %} </ul> {% endif %} {{ form.first_name() }} |
Код выше с user_form в качестве form будет преобразован шаблонизатором в следующую разметку.
1 2 |
<label for="first_name">First name</label> <input id="first_name" name="first_name" type="text" value=""> |
Здесь происходит вот что.
- В первой строке мы обратились к атрибуту label поля first_name нашей формы. В нем содержится HTML-код лейбла нашего поля first_name. Текст берется из описания класса формы из соответствующего атрибута поля.
- Затем мы проверили содержимое списка errors нашего поля. Как нетрудно догадаться, в ней содержатся ошибки. На данный момент ошибок в нем нет, поэтому блок не вывел ничего. Однако если бы эта форма уже заполнялась и была заполнена неверно (например, валидатор от 6 до 30 по длине не пропустил значение), то в список поля попала бы эта ошибка. Мы увидим работу валидаторов дальше.
- И наконец, в последней строке мы рендерим сам тег input, вызывая метод .first_name() нашего инстанса формы.
Все очень гибко. Мы можем рендерить все атрибуты поля или только сам тег input. Нетрудно догадаться, что теперь мы можем сделать то же самое и для всех остальных полей, отрендерив все поля формы или только их часть соответствующими им встроенными HTML-виджетами.
Парсинг пейлоада ([crayon-672b97b8b826b167795060-i/])
Следующий шаг — получить данные формы на сервере и как-то их обработать. Этап состоит из нескольких шагов.
- Получить POST-данные (это может происходить по-разному в зависимости от того, используете ли вы фреймворк и какой конкретно, если используете).
- Распарсить POST-данные через наш инстанс формы.
- Проверить (валидировать) корректность заполнения. Если что-то не так, вернуть ошибки.
- Заполнить данными формы требуемый объект. Это опционально, но, если вы пользуетесь ORM, велика вероятность, что по данным формы вам нужно создать объект в БД.
В нашем случае объект в БД — это пользователь, объект класса User.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
async def create(self, request): # Получаем payload. Для aiohttp это не самый оптимальный # способ для больших payload. Взят для краткости payload = await request.post() # Создаем новый инстанс нашей формы и заполняем его данными, # пришедшими с клиента form = UserForm(payload) # Если данные с клиента проходят валидацию if form.validate(): # Создаем новый объект User user = User() # Заполняем его атрибуты данными формы form.populate_obj(user) # ... # Сохраняем юзера в БД, редиректим дальше |
Мы отрендерили форму, получили данные с клиента обратно, проверили их и записали в БД. При этом мы не погружались во внутренности HTML, ID полей, имена и их сопоставления на клиенте и сервере. Не правда ли, удобно?
Опции для частичного парсинга пейлоада
Если вы внимательно читали предыдущий раздел, у вас непременно возник вопрос: а как модель пользователя заполняется данными формы? Ведь форма ничего не знает о полях ORM (которой может не быть). Так как же происходит маппинг полей формы к объекту в функции populate из WTForms? Проще всего посмотреть код этой функции.
12345678910111213 Signature: form.populate_obj(obj)Source:def populate_obj(self, obj):"""Populates the attributes of the passed `obj`with data from the form's fields.:note: This is a destructive operation;Any attribute with the same nameas a field will be overridden. Use with caution."""for name, field in iteritems(self._fields):field.populate_obj(obj, name)Как видите, функция получает список всех полей нашей формы, а затем, итерируясь по списку, присваивает атрибутам предоставленного объекта значения. Вдобавок ко всему это происходит рекурсивно: это нужно для полей-контейнеров — FormFields.
В большинстве случаев это работает отлично. Даже для полей-ассоциаций: у пользователя может быть поле, значением которого выступает реляция в БД, например группа, к которой принадлежит пользователь. В этом случае воспользуйся классом wtforms.fields.SelectField, передав choices=[...] со списком возможных значений реляций, и на сервере при наличии ORM это будет распознано без проблем.
Однако иногда все-таки нужно автоматически заполнить атрибуты класса только частью полей формы, а остальные как-то препроцессить. Варианта два.
- Не использовать встроенную функцию populate_obj вообще и обрабатывать все поля вручную, получая доступ к ним через атрибут .data каждого поля формы вроде form.f_name.data.
- Написать свой метод для заполнения объекта данными формы.
Мне больше нравится второй вариант (хоть он и имеет ограничения). Например, так:
12345 from wtforms.compat import iteritems, itervalues, with_metaclassdef populate_selective(form, obj, exclude=[]):for name, field in filter(lambda f: f[0] not in exclude, iteritems(form._fields)):field.populate_obj(obj, name)Теперь можно использовать из формы только те поля, которые нужны:
1 populate_selective(form, user, exclude=['f_name', 'l_name', 'city',])А с остальными разбираться по собственной логике.
Валидаторы
Еще один вопрос, ответ на который вы наверняка уже понял по контексту: как работает функция form.validate()? Она проверяет как раз те самые списки валидаторов с параметрами, которые мы указывали при определении класса формы. Давайте попробуем позаполнять различные значения в строковых полях, которые в реальном приложении в нашу форму будет предоставлять с клиента пользователь, и посмотрим, как среагирует валидатор.
1 2 3 4 5 6 |
form = UserForm() form.first_name.data = 'Johnny' form.last_name.data = 'Doe' form.email.data = 'invalid_email' form.password.data = 'super-secret-pass' |
Попробуем валидировать эту форму.
1 2 |
form.validate() # False |
Валидация не прошла. Вы помните, что в каждом поле есть список errors, который будет содержать ошибки, если они произойдут при заполнении формы. Посмотрим на них.
1 2 |
form.first_name.errors # [] |
Все правильно, в первом поле ошибок не было, список валидаторов [validators.Length(min=5, max=30)] пройден, так как имя Johnny удовлетворяет единственному валидатору. Посмотрим другие.
1 2 3 4 5 6 |
form.last_name.errors # ['Field must be between 5 and 30 characters long.'] form.email.errors # ['Invalid email address.'] form.password.errors # [] |
Во втором и третьем случаях сработали валидаторы, а наш шаблон (ниже) выведет список ошибок.
1 2 3 4 5 6 |
{% if form.first_name.errors %} <ul class="errors"> {% for error in form.first_name.errors %} <li>{{ error }}</li>{% endfor %} </ul> {% endif %} |
Разумеется, чтобы все сработало, для повторного дозаполнения формы вам нужно передавать этот же самый инстанс формы в шаблонизатор, а не создавать новый. Кроме списка ошибок, он будет содержать предзаполненные поля с предыдущей попытки, так что пользователю не придется вводить все по новой.
С полным списком встроенных валидаторов можно ознакомиться здесь, а если их не хватит, то WTForms позволяет определить и собственные.
Динамическое изменение свойств полей формы
Вы уже знаете, что у полей формы есть набор общих атрибутов, которые можно указать у всех классов полей. Например, описание, которое идет первым позиционным аргументом в любом поле. Другие примеры:
- id — атрибут ID HTML-виджета при рендеринге;
- name — имя виджета (свойство name в HTML), по которому будет делаться сопоставление;
- ошибки, валидаторы и так далее.
Все это возможно благодаря тому, что все классы полей наследуются от базового класса wtforms.fields.Field.
Однако случаи бывают разные. Иногда может так случиться, что в уже определенной форме нужно поменять значение одного из полей. Для этого нужно:
- для одного из строковых полей нужно установить дефолтное значение;
- для второго поля нужно просто добавить класс при рендеринге (потому что одна и та же форма используется во многих местах в приложении и в этом нужен особый класс);
- и еще для одного поля нужен data-атрибут для клиентского кода со строкой, содержащий API-endpoint для динамического фетчинга данных.
Все эти моменты лучше настраивать прямо перед самым рендерингом формы у готового инстанса формы: совершенно незачем тащить это в определение класса. Но как это сделать? Вспомним, что наша форма — это обычный Python-объект и мы можем управлять его атрибутами!
Зададим дефолтное значение поля first_name (другой вариант — через default):
1 |
form.first_name.data = 'Linus' |
У поля любого класса есть словарь render_kw. Он предоставляет список атрибутов, которые будут отрендерены в HTML-теге (виджете).
1 2 |
# Теперь поле хорошо выглядит с Bootstrap! form.last_name.render_kw['class'] = 'form-control' |
Ну и зададим кастомный data-атрибут для проверки на дублирование аккаунта:
1 |
form.users.render_kw['data-url'] = request.app.router['api_users_search'].url_for() |
Сборные и наследуемые формы
В самом начале статьи мы говорили, что одна и та же форма может использоваться в разных ситуациях. Обычно мы выносим описание формы в отдельный модуль, а затем его импортируем. Но в одном случае у нас должен быть только минимальный набор полей (атрибутов) формы, а в другом — расширенный. Избежать дублирования определений классов форм нам поможет их наследование.
Определим базовый класс формы:
1 2 3 |
class UserBaseForm(Form): email = StringField('Email', [validators.Email()]) password = StringField('Password') |
В нем будут только те поля, которые необходимы для создания пользовательского аккаунта. А затем определим расширенный, который будет наследоваться от базового:
1 2 3 |
class UserExtendedForm(UserBaseForm): first_name = StringField('First name', [validators.Length(min=4, max=25)]) last_name = StringField('Last name', [validators.Length(min=4, max=25)]) |
Создадим две формы и посмотрим, какие поля у них есть.
1 2 3 4 |
base_form = UserBaseForm() base_form._fields # OrderedDict([('email', <wtforms.fields.core.StringField at 0x106b1df60>), # ('password', <wtforms.fields.core.StringField at 0x106b1d630>)]) |
А теперь посмотрим, что содержит наша расширенная форма:
1 2 3 4 5 6 |
extended_from = UserExtendedForm() extended_from._fields # OrderedDict([('email', <wtforms.fields.core.StringField at 0x106b12a58>), # ('password', <wtforms.fields.core.StringField at 0x106b12f60>), # ('first_name', <wtforms.fields.core.StringField at 0x106b12e80>), # ('last_name', <wtforms.fields.core.StringField at 0x106b12ef0>)]) |
Как видите, она содержит не только описанные поля, но и те, которые были определены в базовом классе. Таким образом, мы можем создавать сложные формы, наследуя их друг от друга, и использовать в текущем контроллере ту, которая нам в данный момент необходима.
Другой способ создания сложных форм — уже упомянутый FormField. Это отдельный класс поля, который может наследовать уже существующий класс формы. Например, вместе с Post можно создать и нового User для этого поста, префиксив названиям полей.
Заполнение реляционных полей (one-to-many, many-to-many)
Одна (не)большая проблема при построении форм — это реляции. Они отличаются от обычных полей тем, что их представление в БД не соответствует as is тому, что должно отображаться в поле формы, а при сохранении они могут требовать препроцессинга. И эту проблему легко решить с WTForms. Поскольку мы знаем, что поля формы можно изменять динамически, почему бы не использовать это свойство для ее предзаполнения объектами в нужном формате?
Разберем простой пример: у нас есть форма создания поста и для него нужно указать категорию и список авторов. Категория у поста всегда одна, а авторов может быть несколько. Кстати, схожий способ используется прямо на Xakep.ru (я использую WTForms на бэкенде «Хакера», PHP с WP у нас только в публичной части).
Отображать реляции в форме мы можем двумя способами.
- В обычном <select>, который будет отрендерен как выпадающий список. Этот способ подходит, когда у нас мало возможных значений. Например, список категорий поста — их не более дюжины, включая скрытые.
- В динамически подгружаемом списке, аналогичном списку тегов, которые вы встречаете на других сайтах. Для реализации его нам поможет простой трюк.
В первом варианте у нашей формы есть поле category, в базе оно соответствует полю category_id. Чтобы отрендерить это поле в шаблоне как select, мы должны создать у формы атрибут category класса SelectField. При рендеринге в него нужно передать список из возможных значений, который формируется запросом в БД (читай: список возможных категорий для поста), а также установить дефолтное значение.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# Импортируем хелпер шаблонов, который представляет объект Category # как строку в нужном формате, аналог __str__. Нужно для удобства from admin.template_helpers.categories import humanize_category # Выберем все категории из БД categories = Category.select().all() # Установим дефолтное значение первой из них form.categories.data = [str(categories[0].id)] # Передадим список всех возможных вариантов для SelectField # В шаблоне отрендерится <select> c выбранным указанным <option> # Формат — список кортежей, где (<идентификатор>, <человекочитаемое представление>) form.categories.choices = [(c.id, humanize_category(c)) for c in categories] |
В результате у поля списка появятся предзаполненные значения.
С авторами постов (пользователями) или журналами такой трюк не пройдет. Первых у нас около ста тысяч, и, разумеется, ни рендерить, ни искать в таком гигантском select’е будет невозможно. Один из вариантов решения задачи — использовать библиотеку Select2. Она позволяет превратить любой input в динамически подгружаемый список а-ля список тегов простым присвоением нужного класса, а данные подгружать по предоставленному URL. Мы уже умеем делать это через знакомый словарь render_kw.
1 2 |
form.issues.render_kw['class'] = 'live_multiselect' form.issues.render_kw['data-url'] = request.app.router['api_issues_search'].url_for() |
А дальше простым добавлением в шаблон jQuery-функции превращаем все input c нужным классом в динамически подгружаемые селекторы (обработчик поиска, разумеется, должен быть на сервере):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
$(".live_multiselect").each(function (index) { let url = $(this).data('url') let placeholder = $(this).data('placeholder') $(this).select2({ tags: true, placeholder: placeholder, minimumInputLength: 3, ajax: { url: url, delay: 1000, dataType: 'json', processResults: function (data) { let querySet = { results: data }; return querySet } } }); }); |
В результате получаем удобный переиспользуемый виджет.
Кастомные виджеты и расширения
Пример выше может показаться частным, однако он подводит к важной проблеме. Хорошо, что наша задача решается плагином Select2, который позволяет буквально добавлением одного класса и щепотки JS получить необходимую функциональность. Однако как быть, если нам нужен полностью собственный шаблон для поля или даже полностью свое сложное поле с кастомным шаблоном, поведением и валидаторами?
К счастью, WTForms позволяет создавать нам собственные виджеты (классы-генераторы HTML-шаблонов для рендеринга полей). Мы можем сделать это двумя способами:
- Создать собственный на базе существующего ( class CustomWidget(TextInput):…), расширив его поведение и переопределив методы, включая __call__. Например, обернуть в дополнительный HTML-шаблон.
- Создать полностью собственный виджет, не наследуясь от существующих встроенных.
Список встроенных виджетов можно найти здесь, рекомендации и пример полностью кастомного также присутствуют в документации.
Интегрировать собственный виджет тоже несложно. У каждого поля есть атрибут widget. Мы можем указать наш виджет в качестве этого keyword-аргумента при определении поля в классе формы или, если кастомный виджет нужен не всегда, присваивать его полю динамически.
Кроме кастомных виджетов, мы можем создавать полностью кастомные поля. Примером такого поля служит расширение WTForms-JSON, которое пригодится для обработки JSON-полей моделей. Определить собственное поле также возможно, соответствующий пример вы найдете в документации.
Вместо заключения
Возможно, после прочтения этой статьи вам показалось, что отдельная библиотека для генерации и обслуживания HTML-форм — ненужное усложнение. И будете правы, когда речь идет о небольших приложениях.
Однако, когда вам нужно обрабатывать десяток сложных форм, часть из них переиспользовать и формировать динамически, декларативный способ описания полей и правил их парсинга позволяет не запутаться в бесконечной лапше имен и ID-шников и избавиться от монотонного труда, переложив написание шаблонного кода с программиста на библиотеку. Согласитесь, это же круто.