TATECHATLAS
◎ हिन्दी
प्रोग्रामिंग / मार्गदर्शिका

कस्टम एनकोडर के साथ जटिल पायथन ऑब्जेक्ट्स को JSON में सीरियलाइज़ करना

पायथन का json मॉड्यूल केवल डिक्ट, लिस्ट, टपल, स्ट्रिंग, इंटेजर, फ्लोट, बूल और None को डिफ़ॉल्ट रूप से कन्वर्ट करता है। अन्य प्रकारों के लिए आपको default पैरामीटर या JSONEncoder सबक्लास प्रदान करना होगा। रूंड-ट्रिप फिडेलिटी के लिए object_hook का उपयोग करें।

इस पृष्ठ पर

पायथन का json मॉड्यूल केवल dict, list, tuple, str, int, float, bool, और None को डिफ़ॉल्ट रूप से कन्वर्ट करता है। कुछ भी अन्य होने पर TypeError उठता है जब तक कि आप इसे इंटरसेप्ट न करें। यह इंटरसेप्शन json.dumps के default पैरामीटर या JSONEncoder सबक्लास के default विधि के माध्यम से होता है। दोनों दृष्टिकोण असमर्थित ऑब्जेक्ट प्राप्त करते हैं और एक JSON-संगत मान वापस लाने या विफलता संकेत देने के लिए TypeError उठाने की आवश्यकता होती है। रूंड-ट्रिप फिडेलिटी के लिए आपको decode साइड पर भी एक मिलता हुआ object_hook प्रदान करना होगा जो मार्कर डिक्ट या स्ट्रिंग से मूल प्रकार को पुनर्निर्मित करे।

क्यों JSON मनमाने पायथन ऑब्जेक्ट्स को सीरियलाइज़ नहीं करता

json मॉड्यूल एक स्थिर रूपांतरण तालिका परिभाषित करता है। पायथन डिक्ट को एक JSON ऑब्जेक्ट में मैप करता है, लिस्ट और टपल को एक एरे में मैप करता है, str को एक स्ट्रिंग में मैप करता है, int और float को एक नंबर में मैप करता है, True और False को उनके JSON लिटरल्स में मैप करता है, और None को null में मैप करता है। कॉम्प्लेक्स नंबर्स, datetime इंस्टेंसेस, pathlib.Path ऑब्जेक्ट्स, और यूजर-डिफाइंड क्लास इंस्टेंसेस इस तालिका में कोई एंट्री नहीं रखते हैं। जब एनकोडर तालिका के बाहर एक वैल्यू का सामना करता है तो वह default हुक को कॉल करता है। यदि कोई हुक प्रदान नहीं किया जाता है तो डिफ़ॉल्ट हुक TypeError उठाता है। दस्तावेज़ स्पष्ट रूप से बताता है: एनकोडर केवल सूचीबद्ध प्रकारों का समर्थन करता है और इसे विस्तारित करने के लिए आपको JSONEncoder को सबक्लास करना होगा और एक default विधि लागू करनी होगी।

पायथन दस्तावेज़ से स्रोत उत्खनन: 'इसे अन्य ऑब्जेक्ट्स को पहचानने के लिए विस्तारित करने के लिए, सबक्लास करें और एक default() विधि लागू करें जिसमें एक अन्य विधि हो जो o के लिए एक serializable ऑब्जेक्ट वापस करे यदि संभव हो, अन्यथा उसे सुपरक्लास कार्यान्वयन को कॉल करना चाहिए (TypeError उठाने के लिए).']}, {

import json
# This raises TypeError because complex has no default mapping
try:
    json.dumps(1 + 2j)
except TypeError as e:
    print(e)

जब आप एक कस्टम क्लास को सीरियलाइज़ करते हैं, तो हमेशा एक प्रकार विभेदक कुंजी जैसे __type__ एम्बेड करें ताकि डीकोडर आपके मार्कर डिक्ट को उस साधारण डिक्ट से अलग कर सके जिसमें समान फ़ील्ड नाम हों।

json.dumps में default पैरामीटर के माध्यम से सीरियलाइज़ेशन

default पैरामीटर एक callable स्वीकार करता है जो असमर्थित ऑब्जेक्ट को प्राप्त करता है और एक JSON-serializable मान वापस करता है। वापसी मान एक डिक्ट, एक लिस्ट, एक स्ट्रिंग, एक नंबर, या एक बूलियन हो सकता है। यदि callable ऑब्जेक्ट को संभाल नहीं सकता है तो उसे TypeError उठाना चाहिए ताकि दस्तावेज़ में अन्य ऑब्जेक्ट्स अपने नियमों द्वारा या super कॉल द्वारा अभी भी एनकोड किए जा सकें। यह दृष्टिकोण आदर्श है जब आपको एकल कॉल साइट पर एक-बार परिवर्तन की आवश्यकता होती है और आप एक पुनः उपयोग योग्य क्लास परिभाषित नहीं करना चाहते हैं।

स्रोत दस्तावेज़ canonical उदाहरण के साथ दिखाता है: कॉम्प्लेक्स नंबर्स के साथ: एक फ़ंक्शन isinstance(obj, complex) की जांच करता है, real और imag फ़ील्ड के साथ एक मार्कर डिक्ट वापस करता है, और बाकी सबके लिए TypeError उठाता है।

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))

एक JSONEncoder सबक्लास के माध्यम से व्यवहार का विस्तार

जब वही सीरियलाइज़ेशन तर्क कई कॉल साइट्स के बीच पुनः उपयोग किया जाना चाहिए या जब आपको encoder-स्तर के पैरामीटर जैसे indent या ensure_ascii को कस्टम प्रकार हैंडलिंग के साथ कॉन्फ़िगर करने की आवश्यकता होती है, तो JSONEncoder को सबक्लास करना अधिक स्वच्छ विकल्प है। आप default विधि को ओवरराइड करते हैं और अनहैंडल्ड प्रकारों के लिए super().default(o) को कॉल करते हैं ताकि लाइब्रेरी के अपने संदेश के साथ मानक TypeError उठाया जाए। सबक्लास को json.dumps या json.dump के माध्यम से cls पैरामीटर के माध्यम से पास किया जाता है।

यह पैटर्न एनकोडिंग नीति को कॉल साइट से अलग करता है। आपका एनकोडर क्लास आयात करने वाला कोई भी उपभोक्ता दोहराई गई isinstance चेन के बिना संगत व्यवहार प्राप्त करता है।

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))

एक default फ़ंक्शन और एक JSONEncoder सबक्लास के बीच चयन करना

एक साधारण default फ़ंक्शन का उपयोग करें जब परिवर्तन एक कॉल के लिए विशिष्ट हो, तर्क छोटा हो, और आपको एनकोडर कॉन्फ़िगरेशन साझा करने की आवश्यकता न हो। फ़ंक्शन इन्लाइन पास किया जाता है और कॉल के बाद गायब हो जाता है। एक JSONEncoder सबक्लास का उपयोग करें जब तर्क को पुनः उपयोग किया जाना चाहिए, जब आपको कस्टम प्रकार हैंडलिंग को non-default encoder सेटिंग्स जैसे sort_keys या custom separators टपल के साथ जोड़ना हो, या जब परिवर्तन चेन दो या तीन isinstance चेक से बढ़ जाती है और एक फ्लैट फ़ंक्शन पढ़ने में कठिन हो जाता है।

एक व्यावहारिक थ्रेशोल्ड: यदि आप पाते हैं कि आप एक ही default फ़ंक्शन को dumps में दो से अधिक स्थानों पर पास कर रहे हैं, तो इसे एक क्लास में प्रमोट करें। यदि क्लास केवल default को ओवरराइड करती है और कोई constructor पैरामीटर जोड़ती है, तो फ़ंक्शन रूप अभी भी स्वीकार्य है।

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))

दिनांक, पथ और उपयोगकर्ता-परिभाषित कक्षाओं को एन्कोड करना

datetime ऑब्जेक्ट्स का कोई JSON मैपिंग नहीं होता है। परंपरागत रूप से isoformat विधि द्वारा उत्पन्न ISO 8601 स्ट्रिंग का उपयोग किया जाता है। डिकोडर पक्ष datetime.fromisoformat का उपयोग करके मान को पुनर्निर्मित करता है। pathlib.Path ऑब्जेक्ट्स str(path) के माध्यम से अपने स्ट्रिंग रूप में serialise होते हैं; डिकोडर स्ट्रिंग को वापस Path() के साथ लपेटता है। उपयोगकर्ता-परिभाषित कक्षाओं के लिए अनुशंसित पैटर्न एक मार्कर डिक्शनरी है जिसमें प्रकार विभेदक कुंजी होती है, उदाहरण के लिए __type__ को क्लास नाम पर सेट किया जाता है, साथ ही पुनर्निर्माण के लिए आवश्यक फ़ील्ड।

encode और decode के बीच संगति अत्यंत महत्वपूर्ण है। यदि एन्कोडर {"__type__": "Point", "x": 1, "y": 2} उत्पन्न करता है तो डिकोडर का object_hook __type__ बराबर "Point" के लिए जांच करना चाहिए और संबंधित इंस्टेंस लौटाना चाहिए। बिना किसी विभेदक कुंजी के समान आकार वाला साधारण dict आपकी कस्टम ऑब्जेक्ट से भेद नहीं किया जा सकता।

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)

आउटपुट को प्रभावित करने वाले एन्कोडर पैरामीटर

ensure_ascii नियंत्रित करता है कि गैर-ASCII वर्णों को \uXXXX अनुक्रमों के रूप में escape किया जाए या सीधे उत्पन्न किया जाए। जब आपका कस्टम default गैर-ASCII पाठ वाले स्ट्रिंग्स लौटाता है तो यह सेटिंग बाइट प्रतिनिधित्व को बदलती है लेकिन तार्किक सामग्री को नहीं। indent पढ़ने योग्यता के लिए सफाई जोड़ता है और डिफ़ॉल्ट separators को (',', ':') से (', ', ': ') में बदल देता है। sort_keys डायरेक्ट्री कुंजियों को स्ट्रिंग रूपांतरण से पहले वर्णानुक्रमिक रूप से क्रमबद्ध करता है, जो परीक्षणों में निर्धारित आउटपुट के लिए उपयोगी है। separators आपको (',',':') पास करके कॉम्पैक्ट आउटपुट उत्पादन की अनुमति देता है। allow_nan यह शासन करता है कि NaN, Infinity और -Infinity को जावास्क्रिप्ट लिटरल्स के रूप में एन्कोड किया जाए या ValueError raised हो। check_circular कंटेनर्स में रेफरेंस चक्र का पता लगाता है और सक्षम होने पर ValueError raises करता है; यदि निष्क्रिय है तो चक्र RecursionError का कारण बनता है। skipkeys चुपचाप उन डायरेक्ट्री कुंजियों को गिरा देता है जो str, int, float, bool या None नहीं हैं इसके बजाय TypeError raise किए बिना।

स्रोत उद्धरण: 'यदि check_circular सच है (डिफ़ॉल्ट), तो लिस्ट्स, dicts और कस्टम एन्कोडेड ऑब्जेक्ट्स को अनंत पुनरावृत्ति को रोकने के लिए एन्कोडिंग के दौरान सर्कुलर रेफरेंस के लिए जांच की जाएगी।'

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))

त्रुटि प्रबंधन और पोर्टेबिलिटी सीमाएं

जब default या एन्कोडर की default विधि TypeError raise करती है तो पूरी dumps कॉल विफल हो जाती है; कोई आंशिक आउटपुट नहीं होता है। इसका मतलब है कि पेड़ में कहीं भी एक असमर्थित ऑब्जेक्ट पूरे serialisation को रद्द कर देता है। अपनी default फ़ंक्शन को स्पष्ट संदेश के साथ TypeError raise करने के लिए डिज़ाइन करें जिसमें दोषपूर्ण प्रकार का नाम हो ताकि डिबगिंग तेज हो।

JSON एक framed प्रोटोकॉल नहीं है। एक ही file object के साथ json.dump की बार-बार कॉलें concatenated दस्तावेज़ उत्पन्न करती हैं जो वैध JSON नहीं हैं। कुंजियाँ जो स्ट्रिंग्स नहीं हैं वे round-trip के बाद स्ट्रिंग्स बन जाती हैं, इसलिए loads(dumps(x)) x के बराबर नहीं हो सकता यदि x में integer या tuple keys थे। बहुत बड़े integers और decimal.Decimal values IEEE 754 double-precision उपभोक्ताओं की परिशुद्धता से अधिक हो सकते हैं। दस्तावेज़ीकरण चेतावनी देता है कि malicious JSON डिकोडर को काफी CPU और मेमोरी खपत करने का कारण बन सकता है, इसलिए अप्रत्याशित डेटा को parse करते समय इनपुट आकार को सीमित करें।

स्रोत उद्धरण: 'pickle और marshal के विपरीत, JSON एक framed प्रोटोकॉल नहीं है, इसलिए एक ही fp के साथ dump() की बार-बार कॉल का उपयोग करके कई ऑब्जेक्ट्स को serialise करने की कोशिश करने से एक अमान्य JSON फ़ाइल परिणाम होगी।'

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 JSON

कॉम्प्लेक्स नंबर्स और एक कस्टम क्लास को जोड़ने वाला व्यावहारिक उदाहरण

निम्नलिखित पूर्ण उदाहरण दोनों दृष्टिकोणों को एक साथ दिखाता है। एक standalone default function एक कॉल साइट पर कॉम्प्लेक्स नंबर्स को संभालता है। एक JSONEncoder subclass दूसरी कॉल साइट पर कॉम्प्लेक्स नंबर्स और एक कस्टम Event class को संभालता है, sort_keys के साथ combined जो deterministic output के लिए है। डिकोड पक्ष object_hook का उपयोग करके दोनों प्रकार को उनके मार्कर डिक्शनरी से पुनर्निर्मित करता है।

यह उदाहरण Python 3.6 और बाद के संस्करणों पर चलता है। इसमें कोई थर्ड-पार्टी पैकेज की आवश्यकता नहीं है। एकमात्र पूर्व शर्त isinstance, dictionaries और json मॉड्यूल के बुनियादी API की परिचितता है।

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)

क्या जाँचें

  • json.dumps बिना default या cls के जटिल, datetime, Path, या मनमाने वर्ग उदाहरणों के लिए TypeError उठाता है
  • एक default फ़ंक्शन को अनहैंडल्ड प्रकारों के लिए TypeError उठाना चाहिए; None लौटाने पर चुपचाप null एन्कोड होता है
  • एक JSONEncoder उपवर्ग को अनहैंडल्ड प्रकारों के लिए super().default(o) कॉल करना चाहिए ताकि मानक त्रुटि संदेश बना रहे
  • राउंड-ट्रिप निष्ठा के लिए डिकोड पक्ष पर एक मिलान object_hook आवश्यक है जो मार्कर कुंजी की जाँच करे
  • एक ही फ़ाइल ऑब्जेक्ट पर बार-बार json.dump कॉल अमान्य संयोजित आउटपुट उत्पन्न करती हैं
  • ensure_ascii, indent, sort_keys, और separators प्रतिनिधित्व को प्रभावित करते हैं लेकिन default द्वारा उत्पादित तार्किक सामग्री को नहीं
  • allow_nan=False NaN और Infinity एन्कोडिंग को ValueError में बदल देता है
  • check_circular=True कस्टम-एन्कोडेड ऑब्जेक्ट्स में चक्रों का पता लगाता है और ValueError उठाता है

यह आलेख केवल CPython 3.6 और बाद के संस्करणों पर मानक पुस्तकालय json मॉड्यूल को कवर करता है। यह orjson, ujson, या rapidjson जैसे तृतीय-पक्ष सीरियलाइज़रों को संबोधित नहीं करता है जिनके अपने विस्तार तंत्र हैं। यहां चर्चा की गई राउंड-ट्रिप गारंटी पूरी तरह से आपके एन्कोड मार्करों और डिकोड हुक के बीच संगति पर निर्भर करती है; पुस्तकालय स्वयं कोई प्रकार रजिस्ट्री या स्कीमा सत्यापन प्रदान नहीं करता है। बहुत बड़े पूर्णांक और decimal.Decimal मान IEEE 754 डबल्स के रूप में संख्याओं को पार्स करने वाले JSON उपभोक्ताओं में सटीकता खो सकते हैं। अविश्वसनीय JSON के लिए इनपुट आकार सीमाएं अनुप्रयोग की जिम्मेदारी हैं, मॉड्यूल की नहीं।

स्रोत

  1. Python: json ↗
  2. Python: pathlib ↗
  3. Python: os and working directories ↗
ऊपर जाएँ ↑