Была как-то задача по написанию бота для корпоративного чата, самого бота к сожалению рассмотреть здесь я не могу, но могу выложить интересный пример по генерации json на основе полей класса в python.
Собственно одна из функций бота это создацие индидентов в системе инцидент менеджмента Pager Duty, а также заведение тикетов в YouTrack. В обоих случаях нам надо сгенирировать json для его загрузки в API.
Вот так должне выглядить финальный json для создание инцидента в PD:
{
"incident": {
"type": "incident",
"title": "<override>",
"service": {
"id": "<override>",
"type": "service_reference"
},
"body": {
"type": "incident_body",
"details": "<override>"
},
"urgency": "<override>"
}
}Везде, где есть <override> должно подставится наше кастомное значение.
Вот таким должен выглядеть json для YouTrack:
{
"project": {
"id": "<ID проекта в YT>"
},
"summary": "<Заголовок задачи>",
"description": "<Описание задачи>",
"customFields": [
{
"name": "Assignee",
"type": "SingleUserIssueCustomField",
"value": {
"login": "<Логин пользователя>"
}
},
{
"name": "State",
"type": "StateIssueCustomField",
"value": {
"name": "<Состояние задачи>"
}
}
]
}Конечно оба варианта могут быть значительно больше по количеству элементов, но тут мы рассматриваем не все возможные возможности API того или иного продукта, а как нам на основе полей конструктора сгенерировать json.
Создаем класс для нашей задачи:
from dataclasses import dataclass
import json
@dataclass
class YouTrackBaseFields:
project: dict
summary: str
description: str
customFields: list
def toJson(self):
return json.dumps(self, default=lambda o: o.__dict__, indent=4)
@dataclass
class YouTrackProjectFields:
id: str
@dataclass
class YouTrackCustomFields:
name: str
type: str
value: dict
@dataclass
class YouTrackCustomFieldsValueGen:
def gen_fields(self, **kwargs):
data = dict
if "login" in kwargs:
data = {"login":kwargs["login"]}
elif "name" in kwargs:
data = {"name":kwargs["name"]}
elif "text" in kwargs:
data = {"text":kwargs["text"]}
return data
@dataclass
class PagerDutyBaseFields:
incident: dict
def toJson(self):
return json.dumps(self, default=lambda o: o.__dict__, indent=4)
@dataclass
class PagerDutyIncidentFields:
type: str
title: str
service: dict
body: dict
urgency: str
@dataclass
class PagerDutyBodyFields:
type: str
details: str
@dataclass
class PagerDutyServiceFields:
id: str
type: strРазбираем классы Ютрека:
YouTrackBaseFields:
class YouTrackBaseFields:
project: dict
summary: str
description: str
customFields: listТут мы описываем базовые поля класса, от которых будет по сути идти ответвление дальше. Таким образом это «скелет» для нашего будещего json. Имена полей важны, так как они будут совпадать с тем, что «просочится» в наш json.
В этом же классе мы реализовываем метод toJson, который вернет json.
YouTrackProjectFields:
class YouTrackProjectFields:
id: strимеет одно поле id, которое является словарем.
YouTrackCustomFields:
class YouTrackCustomFields:
name: str
type: str
value: dictТут описываем элементы кастомных полей.
YouTrackCustomFieldsValueGen:
class YouTrackCustomFieldsValueGen:
def gen_fields(self, **kwargs):
data = dict
if "login" in kwargs:
data = {"login":kwargs["login"]}
elif "name" in kwargs:
data = {"name":kwargs["name"]}
elif "text" in kwargs:
data = {"text":kwargs["text"]}
return dataУ Ютрека довольно развесистая структура json, внутри которой есть такие структуры данных как списки, словари и прочее. Код выше упрощает работу с этим и генерирует нам то, как будет выглядеть поле, в зависимости от переданного значения.
Разбираем классы Pager Duty:
PagerDutyBaseFields:
class PagerDutyBaseFields:
incident: dictВ Pager Duty по проще, тут все входит в ключ incident.
PagerDutyIncidentFields:
class PagerDutyIncidentFields:
type: str
title: str
service: dict
body: dict
urgency: strОписываем каркас полей для ключа incident.
PagerDutyBodyFields:
class PagerDutyBodyFields:
type: str
details: strТак как body является словарем, внутри которого два элемента, описываем их отдельным классом.
PagerDutyServiceFields:
class PagerDutyServiceFields:
id: str
type: strТут аналогично классу PagerDutyBodyFields.
Собираем все вместе:
На данном этапе мы описали структуру классов, которая соответствует структуре json. Осталось это правильно использовать.
Давайте создадим main.py и импортируем в него нужные нам классы:
from json_ser import (
PagerDutyBaseFields,
PagerDutyBodyFields,
PagerDutyIncidentFields,
PagerDutyServiceFields,
YouTrackBaseFields,
YouTrackCustomFields,
YouTrackCustomFieldsValueGen,
YouTrackProjectFields
)Далее описываем функцию, которая будет генерировать json для Pager Duty:
def pd_json_template():
body_fields = PagerDutyBodyFields("incident_body", "some details")
service_fields = PagerDutyServiceFields("1234", "service_reference")
incident_fields = PagerDutyIncidentFields("incident", "Some title",
service_fields, body_fields, "some urgency")
main_fields = PagerDutyBaseFields(incident_fields)
payload = main_fields.toJson()
return payloadПеременную body_fields мы делаем экземпляром класса, описывающего структуру поля body. У этого поля есть поля конструктора type и details, именно их мы и передаем в качестве аргументов.
Похожую операцию производим для service_fields, а вот в экземпляр класс incident_fields мы помимо прочего передаем экземпляры класса, созданные выше. И уже в main_fields передаем наш экземпляр incident_fields, который в себе включает все то, что мы описали ранее. Таким образ мы собираем по частям наш json, упаковывая его как бы слоями, разбив перед этим на отдельные «запчасти» и собрав их вместе в конце.
Давайте запустим код и посмотрим вывод:

Отлично, это именно то, что было нужно. Теперь это можно сохранить в переменную и отправить в качестве данных для загрузки в API Pager Duty.
Метод для Ютрека:
def yt_json_tempalte():
project_field = YouTrackProjectFields("1234")
custom_field_value_generator = YouTrackCustomFieldsValueGen()
assaigne_field = YouTrackCustomFields("Assignee", "SingleUserIssueCustomField",
custom_field_value_generator.gen_fields(login="username"))
state_field = YouTrackCustomFields("State", "StateIssueCustomField",
custom_field_value_generator.gen_fields(name="В очереди"))
base_field = YouTrackBaseFields(project_field, "YT summary",
"YT description", [assaigne_field, state_field])
data = base_field.toJson()
return dataЗдесь ситуация аналогичная, мы собираем «по запчастям» json. Если посмотреть на то, как описаны классы и на то, как создаются экземпляры классов и какие параметры в них передаются, все станет понятно.
Запускаем код:

Видим символы, вместо слова «В очереди», которое мы указывали. Дело в том, что по умолчанию при работе с json в python установлен такой параметр — ensure_ascii=True. Это значит, что все символы, которые не являются ASCII, будут экранированы, чтобы гарантировать, что результат будет содержать только ASCII-символы. Это можно оставить как есть, ведь если мы планируем отправлять данные по сети, то так и должно быть, ошибки при загрузке такого json в API не будет. Но если мы хотим обычным print напечатать корректно, то достаточно переопределить этот параметр в методе toJson():
def toJson(self):
return json.dumps(self, default=lambda o: o.__dict__, indent=4, ensure_ascii=False)Выводим json снова:

Все отображается корректно.
Это базовый пример, который показывает как можно использовать классы и json вместе. Это удобно, так как в будущем, чтобы добавить поддержку нового поля например в customFields для Ютрека, достаточно будет просто создать новый экземпляр класса.
Полный файл main.py:
from json_ser import (
PagerDutyBaseFields,
PagerDutyBodyFields,
PagerDutyIncidentFields,
PagerDutyServiceFields,
YouTrackBaseFields,
YouTrackCustomFields,
YouTrackCustomFieldsValueGen,
YouTrackProjectFields
)
def pd_json_template():
body_fields = PagerDutyBodyFields("incident_body", "some details")
service_fields = PagerDutyServiceFields("1234", "service_reference")
incident_fields = PagerDutyIncidentFields("incident", "Some title",
service_fields, body_fields, "some urgency")
main_fields = PagerDutyBaseFields(incident_fields)
payload = main_fields.toJson()
return payload
def yt_json_tempalte():
project_field = YouTrackProjectFields("1234")
custom_field_value_generator = YouTrackCustomFieldsValueGen()
assaigne_field = YouTrackCustomFields("Assignee", "SingleUserIssueCustomField",
custom_field_value_generator.gen_fields(login="username"))
state_field = YouTrackCustomFields("State", "StateIssueCustomField",
custom_field_value_generator.gen_fields(name="В очереди"))
base_field = YouTrackBaseFields(project_field, "YT summary",
"YT description", [assaigne_field, state_field])
data = base_field.toJson()
return data
def main():
print(pd_json_template())
print(yt_json_tempalte())
if __name__ == "__main__":
main()

