파이썬 JSON 직렬화 오류 해결: datetime·Decimal·set 변환 기준

json.dumps()나 json.dump()에서 Object of type datetime is not JSON serializable 오류가 나면, 문제 값만 무조건 str()로 바꾸기보다 JSON으로 내보낼 표현과 원래 의미가 보존되는지를 먼저 정해야 합니다. datetime은 시간대가 드러나는 ISO 문자열인지, Decimal은 정밀도를 지켜야 하는 금액인지, set은 순서가 필요 없는 집합인지에 따라 변환이 달라집니다.

왜 직렬화가 실패할까요?

Python의 기본 JSON 인코더가 바로 다루는 값은 딕셔너리, 리스트, 튜플, 문자열, 정수, 실수, 불리언, None입니다. 이 범위 밖의 객체가 payload나 저장할 딕셔너리에 들어가면 기본 인코더는 TypeError를 냅니다. 따라서 오류를 없애는 일과 데이터 계약에 맞는 표현을 선택하는 일은 나누어 봐야 합니다.

예를 들어 날짜를 문자열로 바꾸는 것은 API가 문자열을 요구할 때는 적절하지만, 나중에 시간대 정보까지 복원해야 하는 데이터라면 변환 형식을 먼저 합의해야 합니다. 운영에서는 다음 순서로 확인하면 판단이 흔들리지 않습니다.

  1. 이 값의 소비자가 기대하는 JSON 타입을 확인합니다.
  2. 원래 값에서 보존해야 할 의미를 적습니다.
  3. 변환 후 다시 읽었을 때 필요한 정보가 남는지 검증합니다.

datetime과 date는 ISO 형식과 시간대를 먼저 확인합니다

date는 isoformat()으로 날짜 문자열을 만들 수 있고, datetime도 isoformat()으로 날짜와 시간을 문자열로 표현할 수 있습니다. 시간대 정보가 있는 aware datetime은 ISO 문자열에 UTC 오프셋을 포함할 수 있지만, naive datetime에는 시간대 정보가 없습니다.

from datetime import date, datetime, timezone
import json

payload = {
    "run_date": date(2026, 10, 15).isoformat(),
    "started_at": datetime.now(timezone.utc).isoformat(),
}

body = json.dumps(payload)

여기서 중요한 것은 isoformat() 호출 자체보다 naive 값과 aware 값을 구분하는 일입니다. 단순히 default=str를 적용하면 오류는 사라질 수 있어도, 어떤 시간대의 시각인지 확인하는 과정이 생략될 수 있습니다. 복원이 필요하다면 소비자가 ISO 문자열을 받는지, datetime.fromisoformat()으로 읽을 수 있는 형식인지까지 확인합니다.

Decimal은 float 변환이 허용되는지 따져 봅니다

Decimal은 십진수의 정밀도와 반올림 규칙을 다루기 위한 타입입니다. 금액이나 정산 값처럼 유효 자릿수가 중요한 값이라면 float로 바꾸는 순간 의미가 달라질 수 있으므로 문자열 표현을 선택하는 편이 안전할 수 있습니다.

from decimal import Decimal
import json

payload = {
    "amount": str(Decimal("10.00")),
}

body = json.dumps(payload)

반대로 소비자가 JSON 숫자를 명시적으로 요구하고 정밀도 손실을 허용한다면 숫자 변환을 검토할 수 있습니다. 다만 Decimal을 어떤 JSON 타입으로 보낼지는 Python 문법의 문제가 아니라 API나 파일 형식의 계약 문제입니다. 금액을 문자열로 보냈다면 소비자가 문자열을 금액으로 해석하는지 확인하고, 숫자로 보냈다면 반올림과 유효 자릿수 규칙을 테스트해야 합니다.

set은 list로 바꾸기 전에 순서 의미를 확인합니다

JSON 배열은 순서가 있는 표현입니다. set은 중복을 제거하는 집합이고 순서를 표현하는 자료구조가 아니므로, 단순히 list(value)로 바꾸면 소비자가 순서에 의미를 부여하는 경우 문제가 생길 수 있습니다.

payload = {
    "labels": sorted({"python", "json", "automation"}),
}

이처럼 결과 순서가 계약에 포함되어야 한다면 정렬 기준을 정하고, 순서가 중요하지 않다면 소비자가 배열 순서를 무시하는지 확인합니다. 이미 집합으로 중복이 제거된 상태라는 사실도 함께 점검해야 합니다. 원래 입력의 중복 여부를 보존해야 한다면 set 자체가 적합한 중간 표현인지부터 다시 봐야 합니다.

중첩 payload에는 제한적인 default를 사용합니다

매번 생성 단계에서 명시적으로 변환하기 어려운 중첩 구조라면 default 함수로 처리 범위를 좁힐 수 있습니다. default는 지원되지 않는 객체를 JSON 인코더가 처리할 수 있는 값으로 바꾸거나, 처리할 수 없으면 TypeError를 발생시키는 역할로 두는 것이 안전합니다.

from datetime import date, datetime
from decimal import Decimal
import json

def json_default(value):
    if isinstance(value, (datetime, date)):
        return value.isoformat()
    if isinstance(value, Decimal):
        return str(value)
    if isinstance(value, set):
        return sorted(value)
    raise TypeError(f"지원하지 않는 타입: {type(value).__name__}")

payload = {"created": datetime(2026, 10, 15), "amount": Decimal("10.00"), "labels": {"python", "json"}}
body = json.dumps(payload, default=json_default)

이 예제의 set 정렬은 서로 비교할 수 있는 문자열을 전제로 합니다. 서로 비교할 수 없는 값이 섞인 집합이라면 데이터 계약에 맞는 별도의 변환과 정렬 기준이 필요합니다. 이 함수는 모든 객체를 문자열로 바꾸는 범용 탈출구가 아닙니다. 타입별 변환 규칙이 데이터 계약과 맞을 때만 사용하고, 처리하지 않은 타입은 조용히 통과시키지 않는 편이 좋습니다. 여러 payload에서 같은 규칙을 재사용하고 정책을 한 곳에 모아야 한다면 JSONEncoder의 default()를 재정의하는 방법을 검토할 수 있습니다.

skipkeys=True는 지원되지 않는 딕셔너리 키를 건너뛰는 옵션일 뿐, 값의 직렬화 문제를 해결하는 변환 규칙은 아닙니다. 키가 사라져도 되는지 명확하지 않다면 사용하지 않는 편이 안전합니다.

변환 후에는 성공 여부보다 의미 보존을 확인합니다

직렬화가 성공했다고 해서 변환이 올바른 것은 아닙니다. 다음 항목을 payload별로 확인하면 임시 수정이 운영 데이터 손실로 이어지는 일을 줄일 수 있습니다.

  • datetime: 문자열에 시간대와 필요한 정밀도가 남아 있는지 확인합니다.
  • Decimal: 소비자가 문자열과 숫자 중 어느 타입을 기대하는지, 반올림 규칙이 맞는지 확인합니다.
  • set: 배열 순서가 필요한지, 중복 제거가 의도된 것인지 확인합니다.
  • 사용자 객체: 복원에 필요한 필드만 명시적으로 내보내고, 알 수 없는 타입은 오류로 남깁니다.
  • 딕셔너리 키: JSON 왕복 뒤 키 타입과 이름이 원래와 같아야 하는지 확인합니다.

특히 default=str는 빠른 로그 출력에는 유용할 수 있어도 API payload의 표준 변환 정책으로 그대로 채택하기에는 정보가 부족합니다. 값별로 허용되는 표현을 정하고, 실제 소비자 계약에 맞는 예제 payload를 검증하는 것이 운영 관점에서 더 안정적입니다.

정리: 명시 변환과 default의 선택 기준

생성 단계에서 어떤 값이 들어오는지 알고 있고 타입별 규칙이 중요하다면 명시적으로 변환합니다. 중첩 구조가 깊고 여러 위치에서 같은 정책을 적용해야 한다면 제한적인 default 함수나 JSONEncoder를 사용하되, 지원 타입 목록 밖의 값은 오류로 남깁니다.

핵심은 오류 메시지를 없애는 것이 아니라 JSON 소비자가 이해할 표현을 선택하는 일입니다. 날짜는 시간대, 금액은 정밀도, 집합은 순서와 중복을 기준으로 판단한 뒤 직렬화된 결과를 다시 읽어 의미가 유지되는지 확인하면 됩니다.

AI 응답을 JSON으로 받아 후속 처리하는 흐름이라면 파이썬 AI JSON 응답 오류 줄이기: JSON 모드와 Structured Outputs, Pydantic 검증 구분법도 다음 점검 순서로 이어서 살펴볼 수 있습니다.

참고 자료

Leave a Comment