나는 이렇게 학습한다/Library

Python JSON 직렬화 TypeError 핸들링하기

daco2020 2023. 9. 26. 15:31

json 변환 시 타입에러 발생

아래와 같이 Decimal 타입을 json 으로 변환하는 코드가 있다고 해보자.

import json
from decimal import Decimal


data = {"value": Decimal("123.456")}

json_str = json.dumps(data)
print(json_str)

 

이를 실행하면 다음의 에러가 발생한다.

    raise TypeError(f'Object of type {o.__class__.__name__} '
TypeError: Object of type Decimal is not JSON serializable

 

왜냐하면 json 라이브러리는 기본적으로 다음 타입들만 직렬화를 지원하기 때문이다.

  • dict
  • list, tuple
  • str
  • int, float, bool
  • None

 

이 문제를 해결하기 위해 다음처럼 코드를 수정할 수 있을 것이다.

import json
from decimal import Decimal


data = {"value": str(Decimal("123.456"))}  # Decimal 을 str 로 바꾼다.

json_str = json.dumps(data)
print(json_str)

# {"value": "123.456"}

 

문제는 해결하였지만 실제 데이터의 구조는 더 복잡하고 어떤 값이 Decimal 타입인지 모두 확인하여 수정해야하는 번거로움이 생긴다.

 

다행스럽게도 json 은 default 파라미터를 두어 이를 쉽게 해결할 수 있도록 지원한다.

 

 

 

default 파라미터 넣기

Python의 json 은 default 파라미터를 통해 커스텀 타입을 어떻게 직렬화할지 지정할 수 있다.

 

예를 들어, Python의 Decimal 타입은 기본적으로 JSON으로 직렬화가 안되지만, default 파라미터에 함수를 지정해주면 그 함수 내에서 Decimal을 문자열로 변환해주는 등의 처리를 할 수 있다.

def default(obj):
    if isinstance(obj, Decimal):
        return str(obj)
    raise TypeError


data = {"value": Decimal("123.456")}

json_str = json.dumps(data, default=default)  # default 파라미터를 추가
print(json_str)  

# {"value": "123.456"}

 

예제 코드를 보면, default 함수 내에서 isinstance(obj, Decimal)로 체크를 해서 만약 그게 Truestr(obj)로 바꿔서 리턴해준다.

 

즉, json.dumps() 함수를 호출하면 Decimal 타입이 문자열로 바뀌어서 JSON 직렬화가 되는 원리이다.

 

추가로 default 함수를 보면 raise TypeError 이 있는데 이는 Decimal 타입이 아닌 set 이나 datetime, date 등의 json 이 지원하지 않는 다른 타입이 오면 에러를 발생시키는 부분이다. 만약 raise 를 하지 않고 None 을 리턴한다면 json 은 이를 정상 값으로 인식해서 "null"로 직렬화한다.

def default(obj):
    if isinstance(obj, Decimal):
        return str(obj)
    return None


data = {"value": datetime.now()}  # value 는 현재시간이어야 한다.

json_str = json.dumps(data, default=default)
print(json_str)

# {"value": null}  # 하지만 null 로 반환된다.

 

참고로, default로 넘어오는 obj는 Python의 json 라이브러리가 JSON으로 직렬화할 수 없는 객체 타입만 넘어온다. 그러니 str, dict, list 같은 기본 타입들까지 default 함수에서 설정해줄 필요는 없다.

 

 

 

default 직렬화 응용하기

default 함수를 활용하면 json 직렬화(변환) 과정에서 추가적인 로직을 수행하는 등의 응용도 할 수 있다. 예를 들어 datetime 객체를 timestamp 형식으로 바꾸어 값을 변환할수도 있다.

def default(obj: Any):
    if isinstance(obj, datetime):
        return obj.timestamp()
    else:
        return "This object cannot be serialized."


data = {"value1": datetime.now(), "value2": uuid.uuid4()}

json_str = json.dumps(data, default=default)
print(json_str)

# {"value1": 1695709237.002437, "value2": "This object cannot be serialized."}

 

또한 어떠한 경우라도 에러를 발생시키지 않아야하는 상황이라면 return "This object cannot be serialized." 처럼 해당 값에 특정 문자열을 저장 시킬 수 있다.

 

즉, 개발 요구사항에 따라 다양하게 default 옵션을 응용하여 사용할 수 있다.

 

이 밖에도 json 은 다양한 옵션 파라미터를 지원하니 더 자세히 알고 싶다면 공식문서를 참고하기 바란다.