파이썬 requests data와 json 차이: API 요청 본문과 Content-Type 확인 순서

파이썬 requests에서 data와 json의 차이는 값을 담는 문법보다 API에 어떤 요청 본문과 Content-Type을 보낼지에 있습니다. API 문서가 application/json을 요구하면 보통 json=payload를 사용하고, 폼 형식이면 data=payload, 파일을 함께 보내는 multipart 요청이면 files=를 검토합니다. 요청이 예상과 다를 때는 응답만 보지 말고 res.request.headers와 res.request.body로 실제 전송 구성을 먼저 확인해야 합니다.

먼저 API 문서에서 요청 형식을 확인합니다

requests.post()를 호출하기 전에 API 문서의 request body 예시와 Content-Type을 한 쌍으로 확인합니다. JSON 객체 예시와 application/json이 함께 제시되었는지, key-value 폼 예시인지, 파일 필드가 있는 multipart 예시인지가 선택 기준입니다.

이 단계에서 중요한 점은 Python 딕셔너리라는 입력 모양만으로 서버가 원하는 형식을 추측하지 않는 것입니다. 같은 딕셔너리라도 data=와 json=에 넣으면 요청 본문이 달라집니다. 실제 API가 요구하는 필드 이름과 형식은 해당 API 문서를 기준으로 판단해야 합니다.

data=, json=, files=는 이렇게 구분합니다

data=: 폼 형식이나 직접 준비한 본문

data에 딕셔너리를 넘기면 requests가 form-encoded 요청 본문으로 인코딩합니다. API 문서가 폼 필드를 요구할 때는 다음처럼 작성합니다.

import requests

payload = {
    "name": "kim",
    "enabled": "true",
}

res = requests.post("https://api.example.com/items", data=payload)
res.raise_for_status()

data는 딕셔너리나 튜플 목록 외에도 bytes와 파일 객체처럼 요청 본문에 직접 보낼 수 있는 값도 받을 수 있습니다. 따라서 data라는 이름만 보고 항상 JSON이라고 생각하면 안 됩니다.

json=: JSON 직렬화가 필요한 API

API가 JSON 본문을 요구한다면 json=payload가 의도를 가장 분명하게 보여 줍니다. requests는 JSON으로 직렬화하고 application/json을 설정합니다.

payload = {
    "name": "kim",
    "enabled": True,
}

res = requests.post("https://api.example.com/items", json=payload)
res.raise_for_status()

Python의 True처럼 Python 객체로 작성한 값을 JSON 본문으로 보내야 할 때 특히 읽기 쉽습니다. 다만 어떤 필드가 필수인지, 값의 타입이 무엇인지는 여전히 API 문서가 정한 계약을 따라야 합니다.

files=: multipart 파일 전송

파일 업로드가 포함된 API라면 files=를 사용해 multipart 전송을 구성합니다.

with open("report.csv", "rb") as report:
    res = requests.post(
        "https://api.example.com/uploads",
        files={"file": report},
        data={"description": "monthly report"},
    )

res.raise_for_status()

파일이 없는 JSON 요청을 files= 방식으로 바꿀 이유는 없습니다. multipart가 필요한지 여부도 API 문서의 Content-Type과 필드 설명으로 확인합니다.

data=json.dumps()를 쓸 때 생기는 함정

기존 코드에서 JSON 문자열을 직접 만들어 data로 넘기는 경우가 있습니다.

import json
import requests

payload = {"name": "kim", "enabled": True}
res = requests.post(
    "https://api.example.com/items",
    data=json.dumps(payload),
)

이 방식은 JSON 문자열을 본문으로 만들지만 Content-Type: application/json을 자동으로 추가하지 않습니다. 서버가 미디어 타입을 기준으로 파싱한다면 본문 내용이 JSON처럼 보여도 요청 형식이 맞지 않을 수 있습니다. 직접 직렬화해야 할 이유가 없다면 json=payload를 우선 검토하고, 꼭 data=json.dumps()를 사용한다면 API 문서에 맞는 헤더를 명시적으로 관리합니다.

res = requests.post(
    "https://api.example.com/items",
    data=json.dumps(payload),
    headers={"Content-Type": "application/json"},
)

여기서도 헤더만 맞추면 충분하다고 단정하지 말고, 서버가 요구하는 JSON 구조와 필드 타입을 함께 확인해야 합니다.

json=과 다른 본문 인자를 섞지 않습니다

json=을 지정하면서 data= 또는 files=도 함께 넘기는 코드는 의도를 다시 확인해야 합니다. Requests 문서와 API 레퍼런스 기준으로 data 또는 files가 함께 지정되면 json=은 무시됩니다.

즉 다음 코드는 JSON과 폼을 합치는 표현이 아닙니다.

res = requests.post(
    "https://api.example.com/items",
    data={"source": "batch"},
    json={"name": "kim"},
)

이 경우 코드에 적힌 json만 보고 JSON이 전송되었다고 판단하면 안 됩니다. 하나의 요청에서 어떤 본문 방식을 쓸지 정하고, 추가 필드가 필요하면 그 방식에 맞춰 구성합니다. 파일과 일반 필드를 함께 보내야 한다면 API가 multipart를 요구하는지 확인한 뒤 files=와 data= 조합을 사용합니다.

실제 전송된 헤더와 body를 확인하는 순서

코드가 그럴듯해 보여도 서버에 도착한 요청을 확인해야 할 때가 있습니다. 응답을 받은 뒤 준비된 요청의 헤더와 body를 기록하면 선택한 인자가 실제 요청으로 어떻게 변환되었는지 볼 수 있습니다.

res = requests.post(
    "https://api.example.com/items",
    json={"name": "kim", "enabled": True},
)

print("request headers:", dict(res.request.headers))
print("request body:", res.request.body)
print("response status:", res.status_code)
print("response text:", res.text)

res.raise_for_status()

확인할 것은 네 가지입니다.

  1. Content-Type이 API 문서의 요구와 같은가
  2. body가 JSON 문자열인지, 폼 인코딩인지, multipart 구성인지
  3. 필드 이름과 값의 타입이 문서의 예시와 같은가
  4. 응답의 상태와 본문이 요청 처리 결과를 설명하는가

응답이 JSON으로 파싱된다는 사실만으로 요청이 성공했다고 판단하지 않는 편이 안전합니다. Requests 공식 문서도 r.json()만으로 성공 여부를 판단하지 말고 status_code 또는 raise_for_status()를 확인하는 흐름을 설명합니다.

운영 코드에서는 요청 body에 비밀번호, 토큰, 개인정보가 들어갈 수 있으므로 그대로 로그에 남기지 말고 민감한 값을 가린 뒤 필요한 헤더와 구조만 기록합니다.

선택과 검증을 한 줄로 정리하면

  • 폼 필드: data=payload
  • JSON 객체: json=payload
  • 파일을 포함한 multipart: files=와 API가 요구하는 추가 필드 구성
  • 직접 JSON 문자열을 만들 때: Content-Type을 별도로 확인
  • 문제가 생겼을 때: res.request.headers, res.request.body, 응답 상태와 본문 순서로 확인

저라면 서버 응답 메시지부터 추측하지 않고 API 문서의 Content-Type을 확인한 다음, 코드의 인자를 하나로 단순화하고 준비된 요청을 출력해 비교하겠습니다. 이 순서면 data가 폼으로 인코딩된 문제인지, JSON 헤더가 빠진 문제인지, 아니면 필드 구조 자체가 다른 문제인지 구분하기 쉬워집니다.

출처

Leave a Comment