Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 837 – 확장 가능한 JSON 직렬화

Author:
Serhiy Storchaka <storchaka at gmail.com>
Discussions-To:
Discourse thread
Status:
Draft
Type:
Standards Track
Created:
12-Jul-2026
Python-Version:
3.16
Post-History:
12-Jul-2026

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

초록

이 PEP는 사용자 지정 수준별로 하나씩, 상호 보완적인 세 부분으로 구성된 json 인코더용 확장 메커니즘을 추가합니다:

  • 라이브러리 수준: 직렬화 프로토콜 — 특수 메서드 __json__()__raw_json__();
  • 애플리케이션 수준: 전역 레지스트리 — copyreg.json(cls, function)copyreg.json_dispatch_table을 채우며, copyreg.pickle()의 선례를 따릅니다;
  • 호출 수준: 인코더별 디스패치 테이블 — json.JSONEncoderdispatch_table 속성으로, pickle.Pickler.dispatch_table을 본뜹니다.

더 구체적인 수준이 우선합니다.

도우미 클래스 copyreg.RawJSON은 이미 인코딩된 JSON 문자열을 감싸 그대로 출력되도록 하며, 이를 통해 다른 방법으로는 표현할 수 없는 표현(예: decimal.Decimal을 완전한 정밀도의 JSON 숫자로 직렬화)을 사용할 수 있습니다.

JSON 형식이 모호하지 않은 여러 표준 라이브러리 컨테이너 형식 — collections.deque, types.MappingProxyType, collections.ChainMap, collections.UserDict, collections.UserListcollections.UserString — 에 __json__ 메서드가 추가되므로 별도 설정 없이 직렬화됩니다.

json 모듈 자체에는 새로운 공개 이름이 추가되지 않습니다.

동기

현재 json.dumps()로 기본 형식(dict, frozendict, list, tuple, str, int, float, bool, None)을 넘어서는 무엇이든 직렬화하려면, 모든 호출에 default= 함수를 전달하거나 json.JSONEncoder를 서브클래싱하고 모든 호출 지점에 해당 서브클래스를 전달해야 합니다. 두 메커니즘 모두 지식을 타입이 아니라 호출에 연결합니다:

  • 이들은 조합되지 않습니다. 각각 사용자 지정 default=가 필요한 두 라이브러리는 애플리케이션이 직접 병합 래퍼를 작성하지 않는 한 하나의 문서에 두 설정을 모두 적용할 수 없습니다. 내부적으로 직렬화하는 라이브러리(로깅, 캐싱, IPC용)는 애플리케이션의 default=를 전혀 볼 수 없습니다.
  • 서드파티 형식은 선택적으로 참여할 수 없습니다. 새 형식을 정의하는 라이브러리는 사용자가 해당 형식을 JSON으로 직렬화할 수 있도록 만들 방법이 없으며, 모든 사용자가 그 방법을 배우고 반복해야 합니다. NumPy 스칼라와 배열(python/cpython#68501, python/cpython#62503), 그리고 D-Bus 정수 형식을 수정해 달라는 요청(python/cpython#77978)은 표준 라이브러리의 범위를 벗어난다는 이유로 — 올바르게도 — 종료되었지만, 근본적인 요구는 충족되지 않은 채 남았습니다.
  • 일부 표현은 불가능합니다. default=는 기본 형식 중 하나를 반환해야 하므로, float가 정확하게 표현할 수 없는 정수가 아닌 JSON 숫자를 출력할 방법이 없습니다. json.loads()parse_float=decimal.Decimal을 통해 십진수를 손실 없이 읽을 수 있지만, 그 결과를 다시 기록할 수는 없습니다(python/cpython#67312). 동일한 제한으로 인해 부동 소수점 형식 지정(python/cpython#81022)과 유한하지 않은 부동 소수점 표현(python/cpython#98306, python/cpython#134717)을 제어할 수 없으며, 미리 인코딩된 JSON을 출력해 달라는 일반적인 요청(python/cpython#86957)도 해결할 수 없습니다.

이러한 요구는 오랫동안 존재해 왔으며 범위도 넓습니다. 관련 TypeError 예외를 발생시키기 전에 확인하는 직렬화 훅에 관한 일반적인 제안들(python/cpython#71549 — 2016년부터 공개 상태, python/cpython#79292, python/cpython#114285, python/cpython#86931) 외에도, 이슈 추적기에는 구체적인 표준 라이브러리 타입을 즉시 지원해 달라는 요청이 누적되었습니다. 여기에는 decimal.Decimal (python/cpython#67312, python/cpython#118810, python/cpython#145115), array.array (python/cpython#70451), collections.deque (python/cpython#64973, python/cpython#73849), types.MappingProxyType (python/cpython#79039), 객체로서의 collections.namedtuple() (python/cpython#67661), datetime.datetime (python/cpython#65742)뿐만 아니라 세트, 프로즌 세트, 바이트 배열 및 이터레이터(python/cpython#75338), 제너레이터(python/cpython#78031), 딕셔너리 뷰(python/cpython#83377)와 같은 전체 범주도 포함됩니다.

이 아이디어는 16년 동안 독립적으로도 제안되어 왔으며, 매번 이 설계의 일부를 새로 고안했습니다. July 2010April 2020의 python-ideas와 discuss.python.org in 2022–2024에서의 __json__() 메서드, python-ideas in 2019에서의 원시 출력 및 표준화된 인코더 프로토콜, discuss.python.org in 2022에서의 등록 함수가 그 예입니다.

한편 생태계는 이 관례를 여러 방식으로 부분적으로 채택했습니다. TurboGears는 인자가 없는 __json__()을 통해 객체를 직렬화하며, 해당 custom_encoders 구성은 이를 우선합니다. 이는 이 PEP와 동일한 순서입니다. Pyramid’s JSON renderer__json__(request)을 호출합니다. 현재 conda, Checkmk, TatSu와 같은 애플리케이션 및 라이브러리는 __json__을 정의합니다. simplejson은 대신 선택적으로 활성화할 수 있는 for_json() 메서드를 추가했으며, 언어에서 이중 밑줄 이름을 예약하기 때문에 정확히 그러한 이름을 선택했습니다(simplejson issue #52). 해당 이슈의 논의에서 GitHub 코드 검색은 2016년 당시 이미 실제 코드에 for_json보다 더 많은 __json__ 정의가 있음을 보여 주었으며, 서드파티 라이브러리가 동일한 시그니처에 의존할 수 있도록 PEP에서 이 프로토콜을 공식화해 달라는 요청이 있었습니다. __json__을 정의할 수 있는 것은 표준 라이브러리뿐이며, 이 PEP가 이를 정의하여 분열을 종식합니다.

형식 지원 요청을 하나씩 단순히 승인할 수는 없었습니다. 요청된 형식 대부분은 JSON 표현이 모호합니다:

  • Decimal — JSON 숫자(1234567890.0987654321)입니까, JSON 문자열("1234567890.0987654321")입니까? 둘 다 흔히 사용되지만, 각각 일부 소비자에게는 잘못된 표현입니다.
  • namedtuple — 튜플이므로 JSON 배열입니까, 아니면 이름이 지정된 필드를 가지므로 JSON 객체입니까?
  • array.array — 숫자로 이루어진 JSON 배열입니까, 아니면 압축 인코딩된 문자열입니까? 합리적인 답은 해당 타입 코드에 따라서도 달라집니다.
  • datetime — 다양한 문자열 표현(ISO 8601 변형, 에포크 초, RFC 형식) 중 어느 것입니까?

표준 라이브러리의 기본값 중 어느 것도 올바를 수 없으므로, 표준 라이브러리는 각 애플리케이션이 자신의 정책을 선언하는 메커니즘을 제공해야 합니다. 이 PEP가 바로 그 메커니즘입니다.

근거

수집된 모든 요구를 포괄하는 하나의 시스템

이 기능은 단일 훅이 아니라 의도적으로 작은 시스템으로 구성됩니다. 각 부분은 위 요청의 서로 다른 군집에 답하며, 어느 한 부분도 모든 요청을 단독으로 포괄하지 않습니다.

  • __json__ 프로토콜은 일반적인 확장 제안과 서드파티 타입 보고에 답합니다. 타입 작성자는 import 없이 한 번만 해당 타입을 직렬화 가능하게 만들 수 있으며, 이는 서브클래스에 상속됩니다.
  • 레지스트리는 요청자가 제어하지 않는 타입이나 그 표현이 정책 선택인 타입(Decimal, namedtuple, array.array, datetime)에 대한 요청에 답합니다. 애플리케이션은 한 줄로 자신의 정책을 선언합니다.
  • 원시 출력(__raw_json__/RawJSON)은 어떤 기본 타입 변환으로도 표현할 수 없는 표현에 대한 요청에 답합니다. 여기에는 사전 인코딩된 조각, 완전한 정밀도의 숫자, 부동 소수점 형식 지정, 유한하지 않은 부동 소수점 수가 포함됩니다.
  • 훅 결과의 덕 타이핑은 범주 요청(이터러블, 집합, 제너레이터, 딕셔너리 뷰, 매핑)에 답합니다. copyreg.json(set, sorted)는 집합에 대한 완전한 해법이며, 훅은 리스트나 딕셔너리를 실체화하는 대신 임의의 이터러블 또는 매핑과 유사한 객체를 반환할 수 있습니다.
  • 표준 라이브러리__json__ 메서드는 모호하지 않은 컨테이너(deque, mappingproxy)에 대한 요청에 직접 답합니다.
  • 인코더별 dispatch_table은 프로세스에서 출력마다 서로 다른 정책이 필요할 때 위 기능 중 어느 것이든 하나의 인코더에 한정합니다.

커스터마이제이션의 세 수준

세 부분은 직렬화가 결정되는 세 수준에 대응하며, 의도적으로 서로 다른 형태를 가집니다.

  • 라이브러리 수준__json__()은 타입 작성자를 위한 것입니다. 메서드에는 import가 필요하지 않고 서브클래스에 상속됩니다. 따라서 라이브러리는 json이나 copyreg에 전혀 의존하지 않고도 자신의 타입을 JSON 직렬화 가능하게 만들 수 있습니다.
  • 애플리케이션 수준copyreg.json()은 자신이 제어하지 않는 타입의 사용자를 위한 것입니다. 애플리케이션은 서드파티 타입이나 모호한 표준 라이브러리 타입을 프로세스 전체에서 직렬화할 방식을 선택합니다. 등록은 정확한 타입을 기준으로 이루어지며, 동일한 처리를 원하는 서브클래스는 대신 __json__을 상속합니다.
  • 호출 수준json.JSONEncoder 서브클래스 또는 인스턴스의 dispatch_table 속성은 특정 호출을 사용자 지정합니다. 예를 들어 하나의 프로그램에서 서로 다른 출력(외부 API와 내부 캐시 등)에 서로 다른 정책이 필요할 때 사용합니다. 기존 default= 매개변수는 다른 메커니즘으로 처리되지 않은 객체를 위한 호출 수준의 포괄 처리기로 남습니다.

더 구체적인 수준이 우선합니다. 레지스트리 항목은 타입의 __json__보다 먼저 조회되며(애플리케이션이 라이브러리를 재정의함), 인코더별 테이블은 전역 레지스트리를 대체합니다(호출이 애플리케이션을 재정의함).

레지스트리가 copyreg에 있는 이유

레지스트리는 json.register()일 수도 있었습니다. 대신 copyreg에 배치한 이유는 세 가지입니다.

가져오기 비용 및 계층화. 외부 타입에 대한 직렬화 함수를 가져오기 시점에 등록하는 라이브러리는 json 모듈에 따른 비용을 부담해서는 안 됩니다. 이 모듈의 가져오기는 re에 의해 대부분의 비용이 발생하며, 이는 어쨌든 copypickle이 가져오는 copyreg를 가져오는 비용의 대략 50배입니다. 레지스트리와 RawJSON을 모두 copyreg에 두면, 등록자는 작은 모듈 하나만 정확히 import합니다. json 모듈은 copy 모듈이 이미 그러하듯 copyreg를 임포트하지만, 그 반대는 결코 일어나지 않습니다.

선례와 대칭성. 이것은 양쪽에 대칭적으로 적용된 pickle 모델입니다. 즉, 전역 테이블을 채우는 등록 함수(copyreg.pickle() / copyreg.json())와 이를 재정의하는 인스턴스별 dispatch_table 속성(pickle.Pickler / json.JSONEncoder)으로 구성됩니다. 함수 이름은 copyreg.pickle()관례를 따릅니다. 30년에 걸친 이 선례는 프로토콜 이름을 딴 등록 함수가 혼동을 일으키지 않음을 보여 줍니다. 등록은 한 번만 수행하는 모듈 접두사 호출이기 때문입니다.

중립 지대. 서드파티 JSON 인코더는 표준 json 모듈을 import하지 않고도 동일한 레지스트리와 헬퍼 클래스를 사용할 수 있으므로, 생태계가 하나의 등록 지점으로 수렴할 수 있습니다.

레지스트리를 copyreg에 배치하면 모듈의 목적도 일반화됩니다 — 자신이 제어하지 않는 타입에 대한 프로토콜 구현을 등록합니다. 다른 프로토콜을 위한 레지스트리(예를 들어 copy 모듈을 위한 __copy____deepcopy__와 유사한 훅)도 같은 패턴에 맞으며 별도로 제안할 예정입니다.

원시 출력 메커니즘

이미 인코딩된 프래그먼트를 그대로 출력하는 것이 기본 타입을 벗어난 표현, 특히 완전한 정밀도의 숫자를 표현하는 유일한 방법입니다. 선택된 메커니즘은 덕 타이핑되는 특수 메서드인 __raw_json__()이며, 편의 래퍼로 copyreg.RawJSON을 사용합니다:

  • 원시 출력을 원하는 훅(또는 __json__)은 copyreg.RawJSON(fragment)를 반환합니다 — 등록자가 이미 지불한 단 한 번의 임포트만 필요합니다 — 또는 임포트가 전혀 필요 없이 __raw_json__만 정의하는 자체 래퍼 클래스의 인스턴스를 반환합니다.
  • RawJSON은 두 번째 기능을 수행하기 위해 self를 반환하는 __json__도 정의합니다. 즉, 훅 결과로만 사용하는 것이 아니라, 사전이나 json.dumps()에 전달된 리스트의 값으로 사전 직렬화된 프래그먼트를 문서에 직접 포함할 수 있습니다.

클래스가 아니라 프로토콜이 인터페이스이며, 클래스는 문법적 설탕입니다.

성능: 불변식으로서의 인코딩 파이프라인

json 인코더는 표준 라이브러리에서 가장 빈번하게 실행되는 코드 중 하나이며, 이 설계에서는 해당 분기 순서를 불변식으로 취급합니다:

  1. 정확히 기본 타입인 객체는 속성 조회를 전혀 수행하지 않고 인코딩됩니다 — 압도적으로 흔한 경우에는 이 PEP에 따른 비용이 전혀 발생하지 않습니다.
  2. 디스패치 테이블과 __json__을 다음으로 조회합니다 — 딕셔너리 조회 한 번과 타입 속성 조회 한 번입니다 — 기본 타입의 서브클래스가 직렬화를 사용자 지정할 수 있도록 isinstance 폴백보다 먼저 수행합니다(그렇지 않으면 int의 서브클래스는 isinstance(o, int)검사에서 소비되며, 이는 IntEnum에 대해 문제의 역사에서 기록한 바로 그 문제입니다: python/cpython#62464).
  3. 그런 다음 isinstance폴백이 사용자 지정하지 않는 서브클래스를 처리하여, 해당 서브클래스에 대해서는 현재 동작을 정확히 보존합니다(IntEnum은 여전히 숫자로 직렬화됩니다).
  4. 훅 결과에 대한 덕 타이핑 — 숫자로서의 __index__, 숫자로서의 __float__, 객체로서 keys()와 함께 사용되는 __iter__, 배열로서 단독으로 사용되는 __iter__, 그대로 출력되는 __raw_json__ — 은 훅이 실행된 경우에만 적용됩니다. 특히 훅에서 나오지 않은 객체에서는 __raw_json__을 절대 조회하지 않으므로, 서브클래스 검사나 default()용으로 향하는 객체에는 추가 조회 비용이 발생하지 않습니다.
  5. default훅은 변경 없이 마지막에 실행됩니다.

기본 __json__을 제공하는 표준 라이브러리 타입

기준은 다음과 같습니다: 표준 라이브러리 타입은 JSON 형식이 모호하지 않을 때에만 기본 ``__json__``을 가집니다. 투명 컨테이너는 해당합니다 — deque, mappingproxy, ChainMap, UserDict, UserList, UserString은 의미상 배열과 객체일 뿐입니다(python/cpython#64973, python/cpython#73849, python/cpython#79039을 직접 해결합니다). Decimal, namedtuple, array.array, datetime은 해당하지 않습니다. Motivation에서 설명한 모호성 때문이며, 이러한 타입에 대한 이 PEP의 답은 애플리케이션에서 한 줄로 등록하는 것입니다(How to Teach This 참조).

이 기준은 향후의 “X에 그냥 __json__을 추가하자”라는 요청에도 즉시 사용할 수 있는 답입니다.

사양

__json__ 프로토콜

클래스는 __json__(self)메서드를 정의할 수 있습니다. json 인코더가 정확히 지원되는 기본 타입이 아니고 디스패치 테이블 항목도 없는 객체를 만나면, 모든 특수 메서드와 마찬가지로 객체의 타입에서 __json__을 조회하고, 존재하면 이를 호출합니다. 그런 다음 객체 대신 그 결과를 직렬화합니다:

  • 기본 타입의 결과 또는 기본 타입 서브클래스의 인스턴스인 결과는 일반적인 방식으로 직렬화됩니다.
  • 그렇지 않으면 결과는 덕 타이핑으로 해석됩니다. __index__가 있는 객체는 operator.index()를 통해 JSON 숫자로 직렬화하고, 그렇지 않으면 __float__이 있는 객체를 float을 통해 JSON 숫자로 직렬화하며, 그렇지 않으면 keys()메서드(및 __getitem__)가 있는 이터러블을 JSON 객체로 직렬화하고, 그렇지 않으면 이터러블을 JSON 배열로 직렬화하며, 그렇지 않으면 타입이 __raw_json__을 정의하는 객체를 이를 호출하여 직렬화합니다(아래 참조). 그 외의 경우에는 TypeError를 발생시킵니다.

__json__의 결과는 프로토콜에 재귀적으로 다시 제출되지 않습니다. 즉, 결과 자체가 훅을 정의하는 타입이어도 다시 변환되지 않습니다.

__raw_json__ 프로토콜

__raw_json__(self)str을 반환해야 하며, 인코더는 내용의 유효성을 검사하지 않고 이를 그대로 출력합니다. 이는 디스패치 테이블 함수 또는 __json__이 생성한 객체에 대해서만 조회됩니다(위의 파이프라인 불변식 참조). 따라서 훅 결과로 사용되는 래퍼 클래스에는 __raw_json__만 필요합니다. 또한 RawJSONself를 반환하는 __json__을 정의하므로, 해당 인스턴스를 입력 문서에 직접 포함할 수 있습니다.

레지스트리

다음 항목이 copyreg에 추가됩니다:

copyreg.json(ob_type, json_function)
공식 등록 인터페이스입니다. json_functionob_type의 직렬화 함수로 등록합니다. json_function이 호출 가능 객체가 아니면 TypeError를 발생시킵니다. 이 함수는 객체만을 유일한 인자로 호출되며, 그 결과는 __json__의 결과와 정확히 동일하게 해석됩니다. 등록은 정확한 타입을 기준으로 이루어지므로 ob_type의 서브클래스에는 적용되지 않습니다.
copyreg.json_dispatch_table
타입을 등록된 함수에 매핑하는 딕셔너리로, json 모듈이 참조하며 레지스트리를 준수하는 다른 JSON 인코더에서도 사용할 수 있습니다. 애플리케이션은 테이블을 직접 변경하는 대신 copyreg.json()을 통해 등록합니다.
copyreg.RawJSON(encoded_json)
인스턴스를 encoded_json을 그대로 출력하여 직렬화하는 래퍼입니다. 인스턴스의 str()는 해당 조각을 반환합니다.

등록된 함수가 __json__보다 우선합니다. 정확히 지원되는 기본 타입 자체에 대한 항목(dict, list, str, int, …)은 이러한 타입의 빠른 경로가 먼저 실행되므로 절대 참조되지 않습니다.

인코더

json.JSONEncoder (및 C 가속기)는 getattr(encoder, 'dispatch_table', copyreg.json_dispatch_table)을 참조하므로, 서브클래스는 dispatch_table 클래스 또는 인스턴스 속성을 설정하여 해당 인코더의 전역 레지스트리를 대체할 수 있으며, 이는 pickle.Pickler와 동일한 방식입니다.

딕셔너리 는 이 PEP의 어떤 부분에도 영향을 받지 않습니다. 키 직렬화는 현재 허용하는 타입만 정확히 허용하며, 키에 대해서는 디스패치 테이블이나 __json__을 참조하지 않습니다(미해결 문제 참조).

check_circular 메커니즘은 변경되지 않습니다. 컨테이너와 default의 결과는 현재와 동일하게 추적됩니다. 훅의 결과는 별도로 추적되지 않으므로, 호출할 때마다 새로운 순환을 반환하는 병적인 훅은 제어되지 않는 재귀와 마찬가지로 결국 RecursionError로 종료됩니다.

표준 라이브러리 __json__ 메서드

명백한 컨테이너의 뷰를 반환하는 __json__collections.dequetypes.MappingProxyType에 추가됩니다(C에서는 self를 반환하며, 인코더는 배열 및 객체 경로를 통해 이를 소비합니다). 또한 collections.ChainMap, collections.UserDict, collections.UserList, collections.UserString에도 추가됩니다(파이썬에서는 self또는 기반이 되는 data를 반환합니다).

하위 호환성

json 모듈에는 새로운 공개 이름이 추가되지 않으며, 새로운 훅을 전혀 정의하지 않는 객체에 대해서는 기존 동작이 변경되지 않습니다:

  • 정확히 지원되는 모든 기본 타입은 이전과 바이트 단위로 동일하게 직렬화됩니다.
  • 훅이 없는 기본 타입의 서브클래스도 이전과 동일하게 직렬화됩니다(IntEnum은 숫자로, str 서브클래스는 문자열로, dict 및 list 서브클래스는 각각 객체와 배열로 직렬화됩니다).
  • 이전에 TypeError로 거부되었던 객체 중 __json__을 정의한 객체는 이제 직렬화됩니다. TurboGears 규칙(인자가 없는 __json__)을 따르는 클래스는 의도한 동작을 정확히 얻습니다. 인자를 받는 Pyramid 렌더러용 클래스의 __json__(self, request)는 계속 TypeError를 발생시킵니다. 이제 default에서가 아니라 누락된 인자에서 발생하므로 예외를 포착하는 코드는 계속 작동하지만 메시지는 변경됩니다. 클래스가 이 이름을 관련 없는 용도로 사용한다면 동작이 변경되지만, 더블 언더스코어 이름은 언어 참조에서 예약되어 있으며 그럴듯한 무관한 의미는 알려져 있지 않습니다.
  • 비공개 _json.make_encoder의 시그니처에 dispatch_table 인자가 추가됩니다.
  • __json__를 갖게 되는 표준 라이브러리의 여섯 클래스는 이전에는 json.dumps()에서 (default=로 처리하지 않는 한) TypeError를 발생시켰습니다. 이를 직렬화하기 위해 default=를 사용하는 코드는 계속 작동합니다. 해당 객체가 더 이상 default에 도달하지 않기 때문입니다. 하지만 내장 표현(array/object)은 사용자 지정 default가 생성한 표현과 다를 수 있습니다. 이는 새로운 기본 동작에서 일반적으로 발생하는 위험이며, 모호하지 않은 컨테이너에서는 허용 가능한 것으로 판단됩니다.
  • from copyreg import json는 가져오는 네임스페이스에서 json모듈 이름을 가립니다. 이는 from copyreg import pickle이 항상 그래 온 것과 같습니다. 문서에서는 모듈 접두사가 붙은 호출을 보여 줍니다.

보안 영향

__raw_json__RawJSON은 검증 없이 문자열을 내보내므로, 공격자가 제어하는 조각을 반환하는 훅은 유효하지 않거나 오해를 일으키는 JSON을 생성할 수 있습니다. 이는 새로운 기능이 아닙니다. default=는 이미 인코딩 중에 임의의 코드를 실행하며, 잘못된 출력을 생성하려면 애플리케이션이 해당 훅을 설치해야 합니다. 문서에서는 원시 조각이 신뢰할 수 있는 생성자에서 제공되어야 한다고 명시합니다.

이 내용을 가르치는 방법

사용자 지정 수준별로 하나씩, 네 가지 레시피가 있습니다.

  1. 라이브러리 수준 — 자체 클래스: __json__를 정의합니다.:
    class Money:
        def __json__(self):
            return {"amount": str(self.amount),
                    "currency": self.currency}
    
  2. 애플리케이션 수준 — 다른 사람이 만든 클래스: 등록합니다.:
    import copyreg, decimal
    copyreg.json(decimal.Decimal, str)
    
  3. 애플리케이션 수준 — JSON으로 달리 표현할 수 없는 표현: 원시 조각을 반환합니다.:
    copyreg.json(decimal.Decimal,
                 lambda d: copyreg.RawJSON(str(d)))
    
  4. 호출 수준 — 하나의 인코더, 서로 다른 정책:
    class APIEncoder(json.JSONEncoder):
        dispatch_table = {decimal.Decimal: str}
    

기존 default=는 다른 메커니즘이 처리하지 못한 객체를 위한 호출 수준의 포괄 처리로 남으며, 마지막에 실행되는 것으로 문서화됩니다.

참조 구현

python/cpython#153607 (브랜치 json-customize4)은 작성자의 2017–2022년 json-customize 브랜치를 리베이스하고 재작업한 것으로, C 및 Python 인코더 지원을 포함하는 프로토콜과 레지스트리, 표준 라이브러리 __json__메서드, 두 인코더 구현 모두에 대한 테스트를 포함합니다. 여기에는 코드 주석으로 기록된 인코딩 파이프라인 불변 조건도 포함됩니다.

거부된 아이디어

copyreg 대신 json.register()
json 모듈에 등록하려면 등록자가 해당 모듈을 임포트해야 하므로(copyreg 임포트 비용의 약 50배이며, 비용의 대부분은 re 때문임), pickle과의 대칭성이 깨지고 서드파티 인코더에 중립적인 레지스트리도 제공되지 않습니다. 검색 가능성 문제는 json 문서에서 copyreg를 가리키도록 하여 해결합니다.
copyreg.register_json()스타일 이름
30년간 이어진 관례인 copyreg.pickle()과 일관되지 않습니다.
Decimal, namedtuple, array.array, datetime에 대한 기본 __json__
각각에는 적어도 두 가지의 정당한 JSON 표현이 있습니다(숫자 대 문자열, 배열 대 객체, 타입 코드에 따른 배열 대 인코딩된 문자열, 그리고 다양한 문자열 형식). 기본값은 많은 소비자에게 잘못된 선택이 될 수 있으므로, 이러한 모호성은 기본값이 아니라 레지스트리를 사용해야 하는 근거입니다.
명시적 옵트인 없이 모든 이터러블과 매핑을 직렬화
이는 python/cpython#75338에서 요청되었고 python/cpython#78031python/cpython#83377에서 암시되었지만, 조용한 동작 변경입니다. __iter__를 갖는 모든 객체(집합, 제너레이터, 파일 객체)가 default=에 도달하거나 예외를 발생시키는 대신 배열로 직렬화되기 시작하기 때문입니다. 따라서 덕 타이핑은 훅의 결과에만 적용되며, 명시적인 옵트인입니다.
__raw_json__대신 isinstance(o, RawJSON)
잘못된 대상에게 부담을 주므로 거부되었습니다. 가져오기를 전혀 하지 않고 __json__를 통해 참여하는 타입은 원시 출력을 필요로 하는 순간 copyreg를 가져와야 합니다. 덕 타이핑 프로토콜에서는 클래스가 편의 기능이지 메커니즘은 아닙니다.
클래스 이름으로 원시 래퍼 인식
이름으로 디스패치하는 선례는 pickle에 있습니다. pickle은 reduce 튜플의 __newobj____newobj_ex__ 호출 가능 객체를 해당 __name__으로 인식하지만, 이 PEP에서는 그 예를 따르지 않습니다. type(o).__name__ == "RawJSON"로 원시 래퍼를 인식하면 임의의 객체에서 평범하고 그럴듯한 클래스 이름을 검사하게 됩니다. 우연히 이 이름을 사용하는 관련 없는 클래스는 인코딩 동작을 아무런 표시 없이 변경하게 됩니다. 덕 타이핑된 __raw_json__은 예약된 더블 언더스코어 네임스페이스에 마커를 유지합니다.
모든 객체에서 __raw_json__을 직접 조회
이렇게 하면 클래스가 단일 메서드로 원시 형식의 자체 직렬화를 수행할 수 있지만, 서브클래스 검사, 덕 타이핑된 해석(__index__, __iter__ 등), default()로 진행하는 동안 모든 객체에 대해 타입 속성 조회가 추가됩니다. 이는 표준 라이브러리에서 가장 빈번한 경로 중 하나에 발생하는 실제 비용입니다. 게이트 방식에서는 대신 원시 객체가 메서드 호출 한 번의 비용을 부담합니다.
__repr__/__str__ 또는 일반적인 __serialize__ 재사용
관련 python/cpython#114285에서 제안되었습니다. 이러한 메서드는 원시 출력의 마커로 사용할 수 없습니다. 거의 모든 타입이 이를 정의하고 그 결과는 대개 유효한 JSON이 아니므로, 인코더는 원시 기능을 지원하는 객체를 구분할 수 없습니다. 형식에 구애받지 않는 __serialize__ 역시 자신이 생성하는 형식이 무엇인지 말할 수 없습니다. 그러나 별도의 마커가 존재한다면 __str__페이로드로 사용할 수 있습니다. Open Issues를 참조하십시오.
레지스트리의 MRO 기반(isinstance) 디스패치
레지스트리는 copyreg.dispatch_table과 같이 정확한 타입 일치를 사용합니다. 서브클래스 디스패치는 프로토콜의 역할입니다. 베이스 클래스가 __json__을 한 번 정의하면 서브클래스가 이를 상속합니다. 정확한 일치를 사용하면 조회가 딕셔너리 접근 한 번으로 끝나며, 빈번한 경로에서 MRO 검색을 피할 수 있습니다.

미해결 문제

  • 딕셔너리 키. 레지스트리와 __json__은 모두 딕셔너리 키에 적용되지 않습니다. 이에 대한 요청이 존재합니다(python/cpython#63020, python/cpython#117391, python/cpython#85741, python/cpython#117592). 호환 가능한 향후 확장에서는 현재 keys must be str...에서 TypeError가 발생하는 바로 그 위치(skipkeys 건너뛰기 전)에서 훅을 조회할 수 있습니다. 그러면 규격을 준수하는 키에는 비용이 발생하지 않으며, 훅 결과는 키 정규화로 다시 들어가고 컨테이너 또는 원시 결과는 거부됩니다. 이 PEP에서는 보류되었습니다.
  • 훅을 통한 순환 참조 감지. 훅이 호출될 때마다 새로 생성된, 같지만 동일하지 않은 순환 구조를 반환하면 Circular reference detected를 보고하는 대신 재귀 제한(RecursionError)이 소진됩니다. check_circular 마커에서 훅 결과를 추적하면 이 문제를 해결할 수 있지만 훅 경로에 어느 정도 비용이 발생합니다. 실제로 중요한 문제라는 증거가 나올 때까지 보류되었습니다.
  • 순수한 마커로서의 __raw_json__. 한 가지 미해결 대안은 __raw_json__을 마커로 유지하되 방출되는 조각을 메서드의 반환값 대신 str(o)에서 가져오는 것입니다. RawJSON이 바로 이 이유로 조각을 반환하는 __str__을 정의하며, 원시 래퍼를 동일성이나 이름으로 인식하는 경우에도 마찬가지입니다. __str__ 슬롯 호출은 전체 메서드 호출보다 빠르지만, 이 설계에서는 모든 원시 래퍼가 __raw_json____str__을 모두 정의해야 합니다.

부록: 관련 이슈

이 PEP에서 참조하는 CPython 이슈를 시간순으로 나열하면 다음과 같습니다.

  • enum.IntEnum은 JSON 직렬화와 호환되지 않음 (python/cpython#62464, 종료됨)
  • json.dumps()는 numpy.ndarray와 numpy.bool_이 직렬화할 수 없다고 주장함 (python/cpython#62503, 종료됨)
  • json.dump()는 딕셔너리 키를 직렬화할 때 ‘default’ 옵션을 무시함 (python/cpython#63020, 미해결)
  • collections.deque는 표준 라이브러리 JSON 직렬 변환기를 기본 제공해야 함 (python/cpython#64973, 미해결)
  • json 라이브러리가 datetime과 같은 객체를 직렬화하지 못함 (python/cpython#65742, 종료됨)
  • json에서 Decimal에 대한 읽기만 지원 (python/cpython#67312, 미해결)
  • namedtuple을 JSON으로 딕셔너리로 인코딩하도록 허용합니다 (python/cpython#67661, 종료됨)
  • json이 numpy.int64를 직렬화하지 못합니다 (python/cpython#68501, 종료됨)
  • array.array를 기본적으로 JSON으로 직렬화합니다 (python/cpython#70451, 열림)
  • TypeError를 발생시키기 전에 obj.__json__을 확인하도록 json.dumps 변경 (python/cpython#71549, 열림)
  • collections.deque를 JSON 직렬화 가능하게 합니다 (python/cpython#73849, 종료됨)
  • set, frozenset, bytearray 및 이터레이터를 JSON 배열로 인코딩합니다 (python/cpython#75338, 종료됨)
  • dbus.Byte에 대한 json 정수 인코딩이 올바르지 않습니다 (python/cpython#77978, 종료됨)
  • 제너레이터를 사용할 때 Json.dump()에 버그가 있습니다 (python/cpython#78031, 종료됨)
  • MappingProxy 객체는 딕셔너리와 똑같이 JSON 직렬화되어야 합니다 (python/cpython#79039, 열림)
  • 사용자 정의 객체 클래스를 JSON 직렬화 가능하게 합니다 (python/cpython#79292, 열림)
  • JSON에서 부동 소수점 인코딩 사용자 지정을 지원합니다 (python/cpython#81022, 열림)
  • json이 딕셔너리 뷰 유형을 인코딩하지 못합니다 (python/cpython#83377, 종료됨)
  • json.JSONEncoder.default는 딕셔너리 키에도 호출되어야 합니다 (python/cpython#85741, 종료됨)
  • 새로운 데이터 모델 메서드 __iter_items__를 도입합니다 (python/cpython#86931, 종료됨)
  • 객체를 str로 json 인코딩할 방법이 없습니다. (python/cpython#86957, 열림)
  • 유한하지 않은 부동 소수점 값의 적절한 JSON 직렬화 또는 사용자 지정 JSON 직렬화를 지원합니다 (python/cpython#98306, 열림)
  • __repr__, __str__ 또는 __serialize__를 사용할 수 있는 경우 이를 사용해 JSON을 인코딩합니다 (python/cpython#114285, 종료됨)
  • JSONEncoder가 TypeError를 발생시키기 전에 지원되지 않는 딕셔너리 키를 .default()를 통해 전달하도록 처리합니다 (python/cpython#117391, 열림)
  • json의 기본 호출 가능 객체/메서드는 키를 무시합니다. (python/cpython#117592, 종료됨)
  • JSON 인코더가 decimal.Decimal 객체를 선택적으로 지원하도록 합니다 (python/cpython#118810, 종료됨)
  • json 모듈에서 NaN 및 Infinity 직렬화를 사용자 지정할 수 있도록 합니다 (python/cpython#134717, 종료됨)
  • json 모듈에서 Decimal을 선택적으로 JSON 숫자로 변환하도록 합니다 (python/cpython#145115, 종료됨)