TypeError: Object of type datetime is not JSON serializable 해결Fixing TypeError: Object of type datetime is not JSON serializable
json.dumps 의 TypeError 를 default=str 한 줄로 덮을지, 타입별 함수를 쓸지 정하게 돕는다.
default=str 은 오류를 없애지만 set·numpy 배열을 문자열로 바꾼다. datetime 은 isoformat, Decimal 은 str, set 은 정렬한 리스트를 돌려주는 default 함수로 타입마다 다룬다.
아래 to_jsonable 함수를 붙여 넣고 json.dumps(데이터, default=to_jsonable) 로 바꾼다.
목차
한 줄 요약:
json.dumps의 TypeError 를default=str로 덮으면 set·numpy 값이 문자열이 되니, 타입별로 값을 돌려주는default함수를 넘긴다.
바로 할 것: 아래 함수를 붙여 넣고 json.dumps(data, default=to_jsonable) 로 부른다.
import json, datetime, decimal, uuid
def to_jsonable(o):
if isinstance(o, (datetime.datetime, datetime.date)):
return o.isoformat() # 2026-11-14T21:30:05
if isinstance(o, decimal.Decimal):
return str(o) # 19.90 을 그대로 둔다
if isinstance(o, (set, frozenset)):
return sorted(o) # JSON 배열로 나간다. 타입이 섞인 set 은 맨 아래 한계 절
if isinstance(o, uuid.UUID):
return str(o)
raise TypeError(f"Object of type {type(o).__name__} is not JSON serializable")
text = json.dumps(data, default=to_jsonable, ensure_ascii=False)
pandas·numpy 값이 있으면 아래 ‘pandas 의 NaT’ 절의 함수로 바꾼다. 환경은 macOS 26.6.2, Python 3.9.6이다. 여기서 JSON(제이슨)은 데이터를 글자로 적는 형식이고, 직렬화는 파이썬 값을 그 글자로 바꾸는 일이다.
오류는 json 이 모르는 타입에서 난다
json.dumps 는 문자열·숫자·불리언·None·리스트(튜플 포함)·딕셔너리만 안다. 그 밖의 값을 만나면 타입 이름을 넣어 이렇게 멈춘다.
TypeError: Object of type datetime is not JSON serializable
같은 문구가 date, Decimal, set, bytes, UUID, PosixPath(Path), numpy 의 int64·ndarray, pandas 의 Timestamp 에서도 났다. 튜플은 오류 없이 [1, 2] 리스트로 나갔다.
파이썬 문서는 default 를 이렇게 설명한다. “It should return a JSON encodable version of the object or raise a TypeError.” 직렬화할 수 없는 값이 올 때만 불리는 함수이고, 바꿀 수 없으면 TypeError 를 다시 일으키라는 뜻이다. 위 함수의 마지막 줄이 그 일을 한다.
위 함수에 없는 타입은 분기를 더한다. bytes 에 return o.decode("utf-8"), Path 에 return str(o) 를 돌려주는 분기를 넣으니 ensure_ascii=False 를 준 json.dumps 에서 {"b": "한글", "p": "/tmp/a"} 가 나왔다(이 인자가 없으면 "\ud55c\uae00" 처럼 이스케이프된다). UTF-8 이 아닌 bytes 는 UnicodeDecodeError 였다.
default=str 은 오류를 없애지만 값을 바꾼다
공개 글 한 곳(bobbyhadz)은 첫 처방으로 default=str 을 든다. 그 글은 isoformat 함수와 JSONEncoder 서브클래스도 뒤에 소개한다. 같은 값을 default=str 로 직렬화해 보니 이랬다.
| 넣은 값 | default=str 결과 |
문제 |
|---|---|---|
datetime(2026,11,14,21,30,5) |
"2026-11-14 21:30:05" |
T 대신 공백이다. 파이썬 3.9 fromisoformat 은 읽었다 |
{1, 2} (set) |
"{1, 2}" |
배열이 아니라 글자 하나다 |
np.array([1, 2]) |
"[1 2]" |
쉼표 없는 글자라 JSON 배열로 못 읽는다 |
np.int64(7) |
"7" |
숫자가 문자열이 됐다 |
b"ab" |
"b'ab'" |
파이썬 표기 그대로다 |
pd.NaT |
"NaT" |
빈 값이 글자 NaT 가 됐다 |
오류는 사라졌지만 받는 쪽이 set 자리에서 배열이 아니라 "{1, 2}" 를 읽는다. 위 함수는 같은 set 을 [1, 2] 로 냈다. default=str 은 오류가 나는 지점을 없애지 않고 뒤쪽으로 미룬다.
마이크로초가 있어도 isoformat() 은 2026-11-14T21:30:05.123456 이고 default=str 은 2026-11-14 21:30:05.123456 였다. 시간대가 있는 값은 isoformat() 에서 2026-11-14T21:30:05+09:00 으로 나갔다.
되읽을 때 datetime 은 돌아오지 않는다
json.loads 는 "2026-11-14T21:30:05" 를 str 로 돌려준다. 날짜로 쓰려면 datetime.datetime.fromisoformat(문자열) 로 한 번 더 바꾼다. 파이썬 3.9 의 fromisoformat 은 끝에 Z 가 붙은 2026-11-14T21:30:05Z 를 ValueError: Invalid isoformat string 으로 거부했다. s.replace("Z", "+00:00") 를 거쳐 넣으면 2026-11-14 21:30:05+00:00 으로 읽혔다.
pandas 의 NaT 는 datetime 이라 isoformat 이 ‘NaT’ 를 낸다
pandas 의 pd.NaT(날짜 빈 값)는 isinstance(pd.NaT, datetime.datetime) 가 True 였고 pd.NaT.isoformat() 은 'NaT' 였다. 위 함수만 쓰면 빈 날짜가 글자 "NaT" 로 나간다. pandas 와 numpy 를 쓰면 NaT 줄과 numpy 줄을 더한 아래 함수로 바꾼다.
import json, datetime, decimal, uuid
import numpy as np
import pandas as pd
def to_jsonable(o):
if o is pd.NaT:
return None # JSON null
if isinstance(o, (datetime.datetime, datetime.date)):
return o.isoformat()
if isinstance(o, decimal.Decimal):
return str(o)
if isinstance(o, (set, frozenset)):
return sorted(o)
if isinstance(o, uuid.UUID):
return str(o)
if isinstance(o, np.generic):
return o.item() # np.int64·np.float32·np.bool_ 등
if isinstance(o, np.ndarray):
return o.tolist()
raise TypeError(f"Object of type {type(o).__name__} is not JSON serializable")
이 함수로 Timestamp 와 NaT 가 든 리스트는 ["2026-11-14T21:30:05", null] 로, np.int64(7) 은 숫자 7 로, np.float32(1.5) 는 1.5 로, np.bool_(True) 는 true 로, 배열은 [1, 2] 로 나갔다. Timestamp·NaT·numpy 값·Decimal·set·UUID 가 든 딕셔너리 하나가 한 번에 직렬화됐다.
표를 JSON 으로 바꿀 땐 df.to_json(orient="records") 가 따로 있다. 기본값은 날짜를 1794691805000(밀리초 숫자)으로 쓰고, date_format="iso" 를 주면 "2026-11-14T21:30:05.000" 이며 NaT 는 null 이었다. 반면 json.dumps(df.to_dict("records")) 는 Timestamp 에서 같은 TypeError 가 났다.
Decimal 은 str, float 는 자릿수를 잃는다
Decimal("19.90") 을 default=str 로 내면 "19.90", default=float 로 내면 19.9 였다. Decimal("12345678901234567890.123456789") 는 float 에서 1.2345678901234567e+19 가 됐다. 금액처럼 자릿수가 의미 있으면 문자열로 내고, 받는 쪽에서 Decimal(back["금액"]) 로 읽는다. 이렇게 하면 Decimal('19.90') 이 그대로 돌아왔다. 숫자로 내야 하는 곳이라면 float 변환으로 값이 바뀌는 것을 받아들인 뒤에 쓴다.
딕셔너리 키는 default 를 거치지 않는다
키가 date 이면 default 를 줘도 TypeError: keys must be str, int, float, bool or None, not datetime.date 가 났다. np.int64 키도 같았다. 키는 {k.isoformat(): v for k, v in d.items()} 처럼 미리 문자열로 바꾼다. 파이썬 문서는 skipkeys=True 이면 그런 키를 오류 대신 건너뛴다고 적는다. 직접 돌려 보지는 않았지만, 문서대로라면 건너뛴 항목은 결과에서 사라진다.
json.dump 는 오류 전에 쓴 만큼 파일에 남긴다
파일에 json.dump({"a": 1, "t": datetime.datetime(2026,11,14)}, f) 로 직접 쓰다 오류가 나면 파일에 {"a": 1, "t": 14글자가 남았다. 반쯤 쓰인 JSON 이다. 파일에 쓸 때는 json.dumps(..., default=to_jsonable) 로 글자를 먼저 만들고 open 을 그 뒤에 두면 오류 때 파일이 아예 생기지 않았다. open 을 먼저 열고 with 안에서 f.write(json.dumps(...)) 로 쓰면 오류 때 크기 0 의 빈 파일이 남았다.
class MyEncoder(json.JSONEncoder) 의 default 메서드에 numpy 정수 분기가 든 함수를 연결하고 cls=MyEncoder 로 넘겨도 같은 결과({"t": "2026-11-14T21:30:05", "n": 3})였다. 시험하지 않은 내 의견이지만, 함수를 여러 곳에서 부를 때만 클래스가 편할 것이다.
시험 범위와 한계
이 글의 값은 macOS 26.6.2 arm64, Python 3.9.6, pandas 2.3.3, numpy 2.0.2 에서 작은 합성 데이터로 시험 스크립트 여섯 개를 두 번씩 돌린 출력이다(두 번의 출력은 같았다). 파이썬 3.10 이상, 윈도, orjson 같은 다른 라이브러리, Flask 같은 프레임워크의 자체 JSON 처리는 시험하지 않았다. 위 함수의 sorted(o) 는 {1, "a"} 처럼 타입이 섞인 set 에서 json 이 아니라 sorted 의 TypeError: '<' not supported between instances of 'str' and 'int' 를 냈다. 그런 set 은 sorted(o, key=str) 로 바꾸면 [1, "a"] 로 나갔지만 {10, 2} 는 글자 순서라 [10, 2] 가 됐다. 공개 글은 bobbyhadz 한 곳만 본문을 읽었고 나머지 검색 결과는 제목만 보았다.
세 줄 요약
default=str은 TypeError 를 없애지만 set 은"{1, 2}", numpy 배열은"[1 2]", 빈 날짜는"NaT"문자열로 바꾼다.- 타입별로 datetime 은
isoformat(), Decimal 은str, set 은sorted리스트를 돌려주고 모르는 타입은 TypeError 로 남기는default함수를 쓴다. - 딕셔너리 키는
default를 안 거치므로 미리 문자열로 바꾸고, 파일에는json.dumps로 완성한 글자를 쓴다.
To help a reader decide whether to cover a json.dumps TypeError with default=str or write a per-type function.
default=str removes the error but turns a set or a numpy array into a string. A default function that returns isoformat for datetime, str for Decimal and a sorted list for set handles each type.
Paste the to_jsonable function below and change the call to json.dumps(data, default=to_jsonable).
Contents
- The error comes from a type json does not know
- default=str removes the error but changes the value
- A datetime does not come back when you read it again
- pandas NaT is a datetime, so isoformat returns ‘NaT’
- Decimal: str is exact, float loses digits
- Dictionary keys do not go through default
- json.dump leaves what it wrote before the error in the file
- Test scope and limits
- Three-line summary
One-line summary: Covering a
json.dumpsTypeError withdefault=strturns set and numpy values into strings, so pass adefaultfunction that returns a suitable value for each type.
Do this first: Paste the function below and call json.dumps(data, default=to_jsonable).
import json, datetime, decimal, uuid
def to_jsonable(o):
if isinstance(o, (datetime.datetime, datetime.date)):
return o.isoformat() # 2026-11-14T21:30:05
if isinstance(o, decimal.Decimal):
return str(o) # keeps 19.90 as it is
if isinstance(o, (set, frozenset)):
return sorted(o) # goes out as a JSON array; a set of mixed types: see the limits section at the bottom
if isinstance(o, uuid.UUID):
return str(o)
raise TypeError(f"Object of type {type(o).__name__} is not JSON serializable")
text = json.dumps(data, default=to_jsonable, ensure_ascii=False)
If you have pandas or numpy values, switch to the function in the “pandas NaT” section below. The environment is macOS 26.6.2 and Python 3.9.6. JSON is a format that writes data as text, and serialization means turning a Python value into that text.
The error comes from a type json does not know
json.dumps knows only strings, numbers, booleans, None, lists (tuples included) and dictionaries. When it meets anything else it stops with the type name in the message.
TypeError: Object of type datetime is not JSON serializable
The same message appeared for date, Decimal, set, bytes, UUID, PosixPath (Path), numpy int64 and ndarray, and pandas Timestamp. A tuple went out as the list [1, 2] without an error.
The Python documentation describes default this way: “It should return a JSON encodable version of the object or raise a TypeError.” It is a function called only when a value cannot be serialized, and if it cannot convert the value it should raise TypeError again. The last line of the function above does that.
For a type the function above lacks, add a branch. With a bytes branch returning o.decode("utf-8") and a Path branch returning str(o), with ensure_ascii=False passed to json.dumps the output was {"b": "한글", "p": "/tmp/a"} (without that argument it is escaped like "\ud55c\uae00"). Bytes that were not UTF-8 raised UnicodeDecodeError.
default=str removes the error but changes the value
One public article (bobbyhadz) gives default=str as its first fix. It also introduces an isoformat function and a JSONEncoder subclass later. Serializing the same values with default=str gave this.
| Value passed | Result with default=str |
Problem |
|---|---|---|
datetime(2026,11,14,21,30,5) |
"2026-11-14 21:30:05" |
A space instead of T. Python 3.9 fromisoformat did read it |
{1, 2} (set) |
"{1, 2}" |
One string, not an array |
np.array([1, 2]) |
"[1 2]" |
No comma, so it cannot be read as a JSON array |
np.int64(7) |
"7" |
The number became a string |
b"ab" |
"b'ab'" |
Python’s own notation |
pd.NaT |
"NaT" |
An empty value became the text NaT |
The error is gone, but the receiving side reads "{1, 2}" where it expects an array. The function above sent the same set as [1, 2]. default=str does not remove the point where the error occurs; it pushes it back to the reading side.
With microseconds, isoformat() gave 2026-11-14T21:30:05.123456 and default=str gave 2026-11-14 21:30:05.123456. A value with a time zone went out of isoformat() as 2026-11-14T21:30:05+09:00.
A datetime does not come back when you read it again
json.loads returns "2026-11-14T21:30:05" as a str. To use it as a date, convert it once more with datetime.datetime.fromisoformat(text). Python 3.9 fromisoformat rejected 2026-11-14T21:30:05Z, which ends in Z, with ValueError: Invalid isoformat string. Passing it through s.replace("Z", "+00:00") first made it read as 2026-11-14 21:30:05+00:00.
pandas NaT is a datetime, so isoformat returns ‘NaT’
pandas pd.NaT (an empty date) gave True for isinstance(pd.NaT, datetime.datetime), and pd.NaT.isoformat() was 'NaT'. With only the function above, an empty date goes out as the text "NaT". If you use pandas and numpy, switch to the function below, which adds a NaT line and numpy lines.
import json, datetime, decimal, uuid
import numpy as np
import pandas as pd
def to_jsonable(o):
if o is pd.NaT:
return None # JSON null
if isinstance(o, (datetime.datetime, datetime.date)):
return o.isoformat()
if isinstance(o, decimal.Decimal):
return str(o)
if isinstance(o, (set, frozenset)):
return sorted(o)
if isinstance(o, uuid.UUID):
return str(o)
if isinstance(o, np.generic):
return o.item() # np.int64·np.float32·np.bool_ 등
if isinstance(o, np.ndarray):
return o.tolist()
raise TypeError(f"Object of type {type(o).__name__} is not JSON serializable")
With this function a list holding a Timestamp and NaT went out as ["2026-11-14T21:30:05", null], np.int64(7) as the number 7, np.float32(1.5) as 1.5, np.bool_(True) as true, and an array as [1, 2]. One dictionary holding a Timestamp, NaT, numpy values, a Decimal, a set and a UUID was serialized in a single call.
To turn a table into JSON there is also df.to_json(orient="records"). By default it writes dates as 1794691805000 (a millisecond number); with date_format="iso" it gave "2026-11-14T21:30:05.000" and NaT became null. By contrast, json.dumps(df.to_dict("records")) raised the same TypeError on Timestamp.
Decimal: str is exact, float loses digits
Decimal("19.90") went out as "19.90" with default=str and as 19.9 with default=float. Decimal("12345678901234567890.123456789") became 1.2345678901234567e+19 through float. When digits matter, as with money, send a string and read it back with Decimal(back["금액"]), which returned Decimal('19.90') unchanged. Where a number is required, use float only after accepting that the value changes.
Dictionary keys do not go through default
With a date key, default did not help and the error was TypeError: keys must be str, int, float, bool or None, not datetime.date. An np.int64 key behaved the same. Convert keys to strings first, as in {k.isoformat(): v for k, v in d.items()}. The Python documentation says that with skipkeys=True such keys are skipped instead of raising an error. I did not run it, but according to the documentation a skipped item is dropped from the result.
json.dump leaves what it wrote before the error in the file
Writing straight to a file with json.dump({"a": 1, "t": datetime.datetime(2026,11,14)}, f) and hitting the error left 14 characters in the file: {"a": 1, "t": — half-written JSON. When writing to a file, build the text first with json.dumps(..., default=to_jsonable) and put open after it; on error the file was not created at all. Opening the file first and writing f.write(json.dumps(...)) inside with left an empty file of size 0 on error.
Connecting a function with the numpy integer branch to the default method of a class MyEncoder(json.JSONEncoder) and passing cls=MyEncoder gave the same result ({"t": "2026-11-14T21:30:05", "n": 3}). This is my untested opinion, but a class is probably handier only when you call the function from several places.
Test scope and limits
The values in this article are the output of six test scripts run twice each on small synthetic data on macOS 26.6.2 arm64, Python 3.9.6, pandas 2.3.3 and numpy 2.0.2 (the two outputs were identical). Python 3.10 or later, Windows, other libraries such as orjson, and the JSON handling built into frameworks such as Flask were not tested. The sorted(o) in the function above did not raise a json error for a set with mixed types like {1, "a"}; it raised sorted’s own TypeError: '<' not supported between instances of 'str' and 'int'. Changing such a set to sorted(o, key=str) produced [1, "a"], but {10, 2} sorted as text and became [10, 2]. Of the public articles, only bobbyhadz was read in full; the other search results were seen by title only.
Three-line summary
default=strremoves the TypeError but turns a set into"{1, 2}", a numpy array into"[1 2]"and an empty date into the string"NaT".- Use a
defaultfunction that returnsisoformat()for datetime,strfor Decimal and a sorted list for set, and leaves unknown types as a TypeError. - Dictionary keys skip
default, so convert them to strings beforehand, and write to files from a finishedjson.dumpsstring.