2025, Nov 01 13:16

Как правильно передавать JSON в requests: dict, json.dumps и альтернативы

Почему не стоит собирать JSON строками в Python. Как передавать данные в requests через dict и json.dumps и когда использовать params, data или json без ошибок.

Когда вы формируете JSON‑тело для requests в Python и пытаетесь подставлять переменные в строку в тройных кавычках, поначалу это кажется удобным. На деле такой подход хрупкий: его легко сломать, он разваливается при малейшем изменении структуры или при появлении символов, требующих экранирования. Типичный сценарий: нужно сделать поля MAC и NAME динамическими при обходе CSV. Правильнее собирать данные как объекты Python и сериализовать их — без ручной разметки JSON.

Пример проблемы

Вот шаблон, который часто вызывает проблемы при попытке превратить переменные в JSON‑тело:

payload = '''{
    "clients": [
        {
            "mac": HW_MAC,
            "name": HOST_ALIAS
        },
    ],}'''

С виду это «как будто JSON», но на самом деле он невалидный, а заполнители не будут восприняты как переменные. К тому же, висячие запятые и неверные скобки ломают разбор.

Что именно идет не так

Ручное форматирование JSON легко приводит к ошибкам. Запятые после последнего элемента недопустимы и вызывают ошибки парсинга. Если попытаться подставлять переменные через f‑строки, фигурные скобки придется удваивать, чтобы их не восприняли как маркеры форматирования. И главное — интерполяция строк не выполнит нужное экранирование: значения с кавычками или спецсимволами дадут невалидный результат. Сериализатор делает это правильно и стабильно.

Решение: соберите объект Python и сериализуйте через json.dumps

Создайте обычный dict или list, затем преобразуйте его в JSON‑строку с помощью json.dumps. Такой подход сохраняет типы данных при обходе, а сериализатор гарантирует валидный JSON и корректное экранирование.

import json

HW_MAC = 'xxxxxxxxxxxx'
HOST_ALIAS = 'example'

request_body = json.dumps({
  "clients": [
    {"mac": HW_MAC, "name": HOST_ALIAS}
  ]
}, indent=2)

print(request_body)

Результат:

{
  "clients": [
    {
      "mac": "xxxxxxxxxxxx",
      "name": "example"
    }
  ]
}

Идя по строкам CSV, присваивайте текущие значения переменным вроде HW_MAC и HOST_ALIAS, заново собирайте dict и снова сериализуйте. Этого достаточно — никакой хрупкой конкатенации и ручных кавычек.

А нужна ли вам вообще JSON‑строка?

В зависимости от метода и эндпоинта HTTP, возможно, вообще не нужно ничего превращать в строку. Например, GET передает параметры через params, а PUT может отправлять form‑encoded данные через data. В обоих случаях requests сам занимается кодированием.

import requests

query_args = {"key1": "value1", "key2": "value2"}
resp = requests.get("https://httpbin.org/get", params=query_args)
import requests

resp = requests.put("https://httpbin.org/put", data={"key": "value"})

Можно передать dict через аргумент json=, и requests сам сериализует его в JSON и автоматически выставит заголовок Content-Type.

import requests

resp = requests.get("https://httpbin.org/get", json={"some": "data"})

Почему это важно

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

Итоги

Перестаньте собирать JSON‑строки вручную для requests. Используйте нативные структуры Python и json.dumps, когда действительно нужна JSON‑строка, либо передавайте данные напрямую через params, data или json — в зависимости от задачи. Так полезные нагрузки остаются валидными, намерения — прозрачными, а код — устойчивым при работе с динамическими значениями вроде MAC и NAME.

Статья основана на вопросе на StackOverflow от Joseph Jenkins и ответе от Mark Tolonen.