Serialisierung komplexer Python-Objekte zu JSON mit benutzerdefinierten Encodern
Das json-Modul behandelt nur eine feste Menge an Python-Typen. Um komplexe Zahlen, Datumsangaben, Pfade oder benutzerdefinierte Klassen zu serialisieren, müssen Sie eine default-Funktion bereitstellen oder JSONEncoder unterklassifizieren und dann die Ausgabe mit object_hook auf der Dekodierseite kombinieren.
Auf dieser Seite
Die kurze Antwort
Das Python json-Modul konvertiert standardmäßig nur dict, list, tuple, str, int, float, bool und None. Alles andere löst einen TypeError aus, es sei denn, Sie fangen ihn ab. Die Abfangung erfolgt über den default-Parameter von json.dumps oder über die default-Methode einer JSONEncoder-Unterklassierung. Beide Ansätze erhalten das nicht unterstützte Objekt und müssen einen JSON-kompatiblen Wert zurückgeben oder TypeError werfen, um ein Scheitern zu signalisieren. Für die Rundlauftreue müssen Sie auch einen passenden object_hook auf der Dekodierseite bereitstellen, der den ursprünglichen Typ aus dem Marker-Dictionary oder String wiederherstellt.
Warum JSON keine beliebigen Python-Objekte serialisiert
Das json-Modul definiert eine feste Konversionstabelle. Python dict wird zu einem JSON-Objekt, list und tuple werden zu einem Array, str wird zu einem String, int und float werden zu einer Zahl, True und False werden zu ihren JSON-Literalen und None wird zu null. Komplexe Zahlen, datetime-Instanzen, pathlib.Path-Objekte und Instanzen benutzerdefinierter Klassen haben keinen Eintrag in dieser Tabelle. Wenn der Encoder einen Wert außerhalb der Tabelle findet, ruft er den default-Hook auf. Wenn kein Hook bereitgestellt wird, wirft der Standard-Hook TypeError. Die Dokumentation stellt dies ausdrücklich fest: Der Encoder unterstützt nur die aufgeführten Typen und um ihn zu erweitern, müssen Sie JSONEncoder unterklassifizieren und eine default-Methode implementieren.
Quellenexcerpt aus der Python-Dokumentation: 'Um dies zu erweitern, um andere Objekte zu erkennen, unterklassifizieren und implementieren Sie eine default()-Methode mit einer anderen Methode, die für o ein serialisierbares Objekt zurückgibt, wenn möglich, andernfalls sollte sie die Implementierung des Superclasses aufrufen (um TypeError zu werfen).'
import json
# This raises TypeError because complex has no default mapping
try:
json.dumps(1 + 2j)
except TypeError as e:
print(e)Praktischer Tipp
Wenn Sie eine benutzerdefinierte Klasse serialisieren, binden Sie immer einen Typdiskriminator-Schlüssel wie __type__ ein, damit der Decoder Ihr Marker-Dictionary von einem normalen Dictionary unterscheiden kann, das zufällig dieselben Feldnamen teilt.
Serialisierung über den default-Parameter in json.dumps
Der default-Parameter akzeptiert einen Aufrufbaren, der das nicht unterstützte Objekt erhält und einen JSON-serialisierbaren Wert zurückgibt. Der Rückgabewert kann ein dict, eine Liste, ein String, eine Zahl oder ein Boolean sein. Wenn der Aufrufbare das Objekt nicht verarbeiten kann, muss er TypeError werfen, damit andere Objekte im selben Dokument weiterhin nach ihren eigenen Regeln oder durch einen geketteten super-Aufruf kodiert werden können. Dieser Ansatz ist ideal, wenn Sie eine einmalige Transformation an einer einzelnen Aufrufstelle benötigen und keine wiederverwendbare Klasse definieren möchten.
Die Quelldokumentation zeigt das kanonische Beispiel mit komplexen Zahlen: Eine Funktion prüft isinstance(obj, complex), gibt ein Markierungs-Dictionary mit real- und imag-Feldern zurück und wirft TypeError für alles andere.
import json
def custom_json(obj):
if isinstance(obj, complex):
return {"__complex__": True, "real": obj.real, "imag": obj.imag}
raise TypeError(f"Cannot serialize object of {type(obj)}")
print(json.dumps(1 + 2j, default=custom_json))Erweiterung des Verhaltens durch eine JSONEncoder-Unterklassierung
Wenn die gleiche Serialisierungslogik an mehreren Aufrufstellen wiederverwendet werden muss oder wenn Sie Encoder-Ebene-Parameter wie indent oder ensure_ascii zusammen mit benutzerdefinierten Typbehandlungen konfigurieren müssen, ist das Unterklassifizieren von JSONEncoder die sauberere Option. Sie überschreiben die default-Methode und rufen super().default(o) für nicht behandelte Typen auf, damit der Standard-TypeError mit der eigenen Nachricht der Bibliothek geworfen wird. Die Unterklassierung wird an json.dumps oder json.dump über den cls-Parameter übergeben.
Dieses Muster trennt die Kodierungsrichtlinie von der Aufrufstelle. Jeder Konsument, der Ihre Encoder-Klasse importiert, erhält konsistentes Verhalten ohne Duplizierung der isinstance-Kette.
import json
class SimpleEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, complex):
return {"__complex__": True, "real": o.real, "imag": o.imag}
return super().default(o)
print(json.dumps({"z": 3 + 4j}, cls=SimpleEncoder))Auswahl zwischen einer default-Funktion und einer JSONEncoder-Unterklassierung
Verwenden Sie eine einfache default-Funktion, wenn die Transformation spezifisch für einen Aufruf ist, die Logik kurz ist und Sie keine Encoder-Konfiguration teilen müssen. Die Funktion wird inline übergeben und verschwindet nach dem Aufruf. Verwenden Sie eine JSONEncoder-Unterklassierung, wenn die Logik wiederverwendet werden muss, wenn Sie benutzerdefinierte Typbehandlung mit nicht-standardmäßigen Encoder-Einstellungen wie sort_keys oder einem benutzerdefinierten separators-Tupel kombinieren möchten oder wenn die Transformationskette mehr als zwei oder drei isinstance-Prüfungen wächst und eine flache Funktion schwer lesbar wird.
Eine praktische Schwelle: Wenn Sie feststellen, dass Sie dieselbe default-Funktion an mehr als zwei Stellen an dumps übergeben, befördern Sie sie zu einer Klasse. Wenn die Klasse nur default überschreibt und keine Konstruktorparameter hinzufügt, ist die Funktionsform immer noch akzeptabel.
import json
from datetime import datetime, timezone
class ProjectEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, datetime):
return o.isoformat()
if isinstance(o, complex):
return {"__complex__": True, "real": o.real, "imag": o.imag}
return super().default(o)
payload = {"ts": datetime(2024, 1, 15, tzinfo=timezone.utc), "z": 1+1j}
print(json.dumps(payload, cls=ProjectEncoder, sort_keys=True))Kodierung von Datumsangaben, Pfaden und benutzerdefinierten Klassen
datetime-Objekte haben keine JSON-Zuordnung. Die konventionelle Darstellung ist der ISO 8601-String, der von der isoformat-Methode erzeugt wird. Die Decoderseite verwendet datetime.fromisoformat, um den Wert wiederherzustellen. pathlib.Path-Objekte serialisieren ihre Stringform via str(path); der Decoder wickelt den String wieder mit Path() ein. Für benutzerdefinierte Klassen ist das empfohlene Muster ein Markierungs-Dictionary, das einen Typdiskriminator-Schlüssel enthält, zum Beispiel __type__ gesetzt auf den Klassennamen, plus die Felder, die für die Wiederherstellung benötigt werden.
Konsistenz zwischen Encode und Decode ist kritisch. Wenn der Encoder {"__type__": "Point", "x": 1, "y": 2} ausgibt, muss der object_hook des Decoders auf __type__ gleich "Point" prüfen und die entsprechende Instanz zurückgeben. Ohne einen Diskriminator-Schlüssel wäre ein normales Dictionary mit derselben Form von Ihrem benutzerdefinierten Objekt ununterscheidbar.
import json
from datetime import datetime, timezone
from pathlib import Path
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def encode(obj):
if isinstance(obj, datetime):
return obj.isoformat()
if isinstance(obj, Path):
return str(obj)
if isinstance(obj, Point):
return {"__type__": "Point", "x": obj.x, "y": obj.y}
raise TypeError(f"Unsupported type {type(obj)}")
def decode(dct):
if dct.get("__type__") == "Point":
return Point(dct["x"], dct["y"])
return dct
p = Point(3, 4)
s = json.dumps(p, default=encode)
print(s)
restored = json.loads(s, object_hook=decode)
print(type(restored).__name__, restored.x, restored.y)Encoder-Parameter, die die Ausgabe beeinflussen
ensure_ascii steuert, ob Nicht-ASCII-Zeichen als \uXXXX-Sequenzen escaped werden oder direkt ausgegeben werden. Wenn Ihre benutzerdefinierte default Strings mit Nicht-ASCII-Text zurückgibt, ändert diese Einstellung die Byte-Repräsentation, aber nicht den logischen Inhalt. indent fügt Leerzeichen für Lesbarkeit hinzu und ändert die Standardseparatoren von (',', ':') zu (', ', ': '). sort_keys sortiert Dictionary-Schlüssel alphabetisch vor der Stringkonvertierung, was für deterministische Ausgabe in Tests nützlich ist. separators ermöglicht Ihnen, kompakte Ausgabe zu produzieren, indem Sie (',',':') übergeben. allow_nan regelt, ob NaN, Infinity und -Infinity als JavaScript-Literale kodiert werden oder ValueError werfen. check_circular erkennt Referenzzyklen in Containern und wirft ValueError bei Aktivierung; wenn deaktiviert, verursacht ein Zyklus RecursionError. skipkeys lässt Dictionary-Schlüssel stillschweigend fallen, die nicht str, int, float, bool oder None sind, statt TypeError zu werfen.
Quellenexcerpt: 'Wenn check_circular wahr ist (der Standard), dann werden Listen, Dictionaries und benutzerdefinierte kodierte Objekte während der Kodierung auf zirkuläre Referenzen überprüft, um eine unendliche Rekursion zu verhindern.'
import json
from datetime import datetime
class Enc(json.JSONEncoder):
def default(self, o):
if isinstance(o, datetime):
return o.isoformat()
return super().default(o)
print(json.dumps({"b": 1, "a": datetime(2024,1,1)}, cls=Enc, sort_keys=True, indent=2))Fehlerbehandlung und Portabilitätsbeschränkungen
Wenn default oder die default-Methode des Encoders TypeError wirft, schlägt der gesamte dumps-Aufruf fehl; es gibt keine teilweise Ausgabe. Das bedeutet, dass ein einziges nicht unterstütztes Objekt irgendwo im Baum die gesamte Serialisierung abbricht. Entwerfen Sie Ihre default-Funktion so, dass sie TypeError mit einer klaren Nachricht wirft, die den fehlerhaften Typ nennt, damit das Debugging schnell ist.
JSON ist kein gerahmtes Protokoll. Wiederholte Aufrufe von json.dump mit demselben Dateiobjekt erzeugen verkettete Dokumente, die kein gültiges JSON sind. Schlüssel, die keine Strings sind, werden nach dem Rundlauf zu Strings, also loads(dumps(x)) kann nicht gleich x sein, wenn x ganzzahlige oder Tupel-Schlüssel hatte. Sehr große Ganzzahlen und decimal.Decimal-Werte können die Präzision von IEEE 754 Doppelpräzisionskonsumenten überschreiten. Die Dokumentation warnt davor, dass bösartiges JSON den Decoder veranlassen kann, beträchtliche CPU und Speicher zu verbrauchen, also begrenzen Sie die Eingabegröße beim Parsen von nicht vertrauenswürdigen Daten.
Quellenexcerpt: 'Im Gegensatz zu pickle und marshal ist JSON kein gerahmtes Protokoll, sodass der Versuch, mehrere Objekte mit wiederholten Aufrufen von dump() unter Verwendung desselben fp zu serialisieren, zu einer ungültigen JSON-Datei führt.'
import json, io
buf = io.StringIO()
json.dump({"a": 1}, buf)
json.dump({"b": 2}, buf) # second call makes the file invalid
print(buf.getvalue()) # {"a": 1}{"b": 2} - not valid JSONPraktisches Beispiel, das komplexe Zahlen und eine benutzerdefinierte Klasse kombiniert
Das folgende vollständige Beispiel demonstriert beide Ansätze nebeneinander. Eine standalone default-Funktion behandelt komplexe Zahlen an einer Aufrufstelle. Eine JSONEncoder-Unterklassierung behandelt komplexe Zahlen und eine benutzerdefinierte Event-Klasse an einer anderen Aufrufstelle, kombiniert mit sort_keys für deterministische Ausgabe. Die Decoderseite verwendet object_hook, um beide Typen aus ihren Marker-Dictionarys wiederherzustellen.
Dieses Beispiel läuft auf Python 3.6 und später. Es erfordert keine Drittanbieter-Pakete. Die einzige Voraussetzung ist Vertrautheit mit isinstance, Dictionaries und der grundlegenden API des json-Moduls.
import json
class Event:
def __init__(self, name, ts, payload):
self.name = name
self.ts = ts
self.payload = payload
# Approach 1: standalone default function
def complex_default(obj):
if isinstance(obj, complex):
return {"__complex__": True, "real": obj.real, "imag": obj.imag}
raise TypeError(f"Cannot serialize {type(obj)}")
s1 = json.dumps(1 + 2j, default=complex_default)
print(s1)
# Approach 2: reusable JSONEncoder subclass
class AppEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, complex):
return {"__complex__": True, "real": o.real, "imag": o.imag}
if isinstance(o, Event):
return {"__type__": "Event", "name": o.name,
"ts": o.ts, "payload": o.payload}
return super().default(o)
s2 = json.dumps({"z": 3+4j, "ev": Event("click", 100, {"x": 1})},
cls=AppEncoder, sort_keys=True)
print(s2)
# Round-trip decode
def app_hook(dct):
if dct.get("__complex__"):
return complex(dct["real"], dct["imag"])
if dct.get("__type__") == "Event":
return Event(dct["name"], dct["ts"], dct["payload"])
return dct
restored = json.loads(s2, object_hook=app_hook)
print(type(restored["z"]).__name__, restored["ev"].name)Was Sie prüfen sollten
- json.dumps ohne default oder cls wirft TypeError für complex, datetime, Path oder beliebige Klasseninstanzen
- Eine default-Funktion muss TypeError für nicht behandelte Typen werfen; None zurückzugeben kodiert stillschweigend null
- Eine JSONEncoder-Unterklassierung muss super().default(o) für nicht behandelte Typen aufrufen, um die Standardfehlermeldung beizubehalten
- Rundlauftreue erfordert einen passenden object_hook auf der Dekodierseite, der den Markerschlüssel prüft
- Wiederholte json.dump-Aufrufe auf dasselbe Dateiobjekt erzeugen invalid verkettete Ausgabe
- ensure_ascii, indent, sort_keys und separators beeinflussen die Repräsentation, nicht aber den logischen Inhalt, der von default produziert wird
- allow_nan=False wandelt NaN und Infinity Encoding in ValueError um
- check_circular=True erkennt Zyklen in benutzerdefiniert kodierte Objekten und wirft ValueError
Geltungsbereich
Dieser Artikel behandelt nur das Standardbibliotheks-Modul json auf CPython 3.6 und später. Er adressiert keine Drittanbieter-Serialisierer wie orjson, ujson oder rapidjson, die ihre eigenen Erweiterungsmechanismen haben. Die hier diskutierten Rundlaufgarantien hängen vollständig von der Konsistenz zwischen Ihren Encode-Markern und Decode-Hooks ab; die Bibliothek selbst bietet kein Typregister oder Schema-Validierung. Sehr große Ganzzahlen und decimal.Decimal-Werte können die Präzision von JSON-Konsumenten verlieren, die Zahlen als IEEE 754 Doppelwerte parsen. Eingabegrößenbegrenzungen für nicht vertrauenswürdiges JSON liegen in der Verantwortung der Anwendung, nicht des Moduls.