Skip to main content

Powerful universal JSON encoder/decoder for Python objects with support for pydantic v2.

Project description

๐Ÿš€ Kajson - Universal JSON Encoder/Decoder for Python

PyPI version Python versions License Documentation Discord Website

Kajson is a powerful drop-in replacement for Python's standard json module that automatically handles complex object serialization, including Pydantic v2 models, datetime objects, and custom types.

This library is used by Pipelex, the open-source language for repeatable AI workflows, check it out.

โœจ Why Kajson?

Say goodbye to type X is not JSON serializable!

The Problem with Standard JSON

import json
from datetime import datetime
from pydantic import BaseModel

class User(BaseModel):
    name: str
    created_at: datetime

user = User(name="Alice", created_at=datetime.now())

# โŒ Standard json fails
json.dumps(user)  # TypeError: Object of type User is not JSON serializable

The Kajson Solution

Full example: ex_08_readme_basic_usage.py

import kajson

# โœ… Just works!
json_str = kajson.dumps(user)
restored_user = kajson.loads(json_str)
assert user == restored_user  # Perfect reconstruction!

๐ŸŽฏ Key Features

  • ๐Ÿ”„ Drop-in replacement - Same API as standard json module
  • ๐Ÿ Pydantic v2 support - Seamless serialization of Pydantic models
  • ๐Ÿ“… DateTime handling - Built-in support for date, time, datetime, timedelta
  • ๐Ÿ—๏ธ Type preservation - Automatically preserves and reconstructs original types
  • ๐Ÿ›๏ธ Class registry - Handle dynamic classes from distributed systems and runtime generation
  • ๐Ÿ”Œ Extensible - Easy registration of custom encoders/decoders
  • ๐ŸŽ Batteries included - Common types work out of the box

๐Ÿ“ฆ Installation

# Using pip
pip install kajson

# Using poetry
poetry add kajson

# Using uv (recommended)
uv pip install kajson

๐Ÿš€ Quick Start

Basic Usage

Full example: ex_01_basic_pydantic_serialization.py

from datetime import datetime
from pydantic import BaseModel
from kajson import kajson, kajson_manager

class User(BaseModel):
    name: str
    email: str
    created_at: datetime

def main():
    # Create and serialize
    user = User(
        name="Alice", 
        email="alice@example.com", 
        created_at=datetime.now()
    )

    # Serialize to JSON
    json_str = kajson.dumps(user, indent=2)

    # Deserialize back
    restored_user = kajson.loads(json_str)
    assert user == restored_user  # โœ… Perfect reconstruction!

if __name__ == "__main__":
    kajson_manager.KajsonManager()
    main()

Working with Complex Nested Models

Full example: ex_09_readme_complex_nested.py

from datetime import datetime
from typing import Any, Dict, List
from pydantic import BaseModel

class Comment(BaseModel):
    author: str
    content: str
    created_at: datetime

class BlogPost(BaseModel):
    title: str
    content: str
    published_at: datetime
    comments: List[Comment]
    metadata: Dict[str, Any]

# Create complex nested structure
post = BlogPost(
    title="Introducing Kajson",
    content="A powerful JSON library...",
    published_at=datetime.now(),
    comments=[
        Comment(author="Alice", content="Great post!", created_at=datetime.now()),
        Comment(author="Bob", content="Very helpful", created_at=datetime.now())
    ],
    metadata={"views": 1000, "likes": 50}
)

# Serialize and deserialize - it just works!
json_str = kajson.dumps(post)
restored_post = kajson.loads(json_str)

# All nested objects are perfectly preserved
assert isinstance(restored_post.comments[0], Comment)
assert restored_post.comments[0].created_at.year == datetime.now().year

๐Ÿค Compatibility

  • Python: 3.10 โ€“ 3.14 (3.9 was dropped in the latest release)
  • Pydantic: v2.x
  • Dependencies: Minimal, only standard library + pydantic

๐Ÿ”„ Migration from Standard JSON

Migrating is as simple as changing your import:

# Before
import json
data = json.dumps(my_object)  # Often fails with complex objects

# After  
import kajson as json  # Drop-in replacement!
data = json.dumps(my_object)  # Works with complex objects

Or use Kajson's convenience functions directly:

import kajson
data = kajson.dumps(my_object)

๐Ÿ—๏ธ How It Works

Kajson extends the standard JSON encoder/decoder by:

  1. Type Preservation: Adds __class__ and __module__ metadata to JSON objects
  2. Smart Decoding: Automatically reconstructs original Python objects
  3. Registry System: Allows registration of custom encoders/decoders
  4. Pydantic Integration: Special handling for Pydantic models and validation
  5. Class Registry: Maintains a registry of dynamically created classes that aren't available in standard module paths, enabling serialization/deserialization in distributed systems and runtime scenarios

โš ๏ธ Security Considerations

Warning: Instantiating classes using __class__ and __module__ attributes poses a security threat when deserializing untrusted JSON data. Malicious JSON could potentially instantiate arbitrary classes and execute code.

Only use Kajson to deserialize JSON from trusted sources. For untrusted data, consider:

  • Validating JSON structure before deserialization
  • Using a whitelist of allowed classes
  • Sanitizing input data

For more discussion on this topic, see this discussion thread.

๐Ÿ“š Use Cases

  • REST APIs: Serialize Pydantic models for API responses
  • Data Persistence: Save complex objects to JSON files
  • Message Queues: Send rich objects through Redis/RabbitMQ
  • Configuration: Store config with proper types
  • Data Science: Serialize numpy arrays, pandas DataFrames (with custom encoders)

๐Ÿ”ง Advanced Features

Custom Type Registration

Full example: ex_10_readme_custom_registration.py

Register encoders/decoders for any type:

from decimal import Decimal
from pathlib import Path
from typing import Any, Dict
import kajson

# Register Decimal support
def encode_decimal(value: Decimal) -> Dict[str, str]:
    return {"decimal": str(value)}

def decode_decimal(data: Dict[str, str]) -> Decimal:
    return Decimal(data["decimal"])

kajson.UniversalJSONEncoder.register(Decimal, encode_decimal)
kajson.UniversalJSONDecoder.register(Decimal, decode_decimal)

# Now Decimal works seamlessly
data = {"price": Decimal("19.99"), "tax": Decimal("1.50")}
json_str = kajson.dumps(data)
restored = kajson.loads(json_str)
assert restored["price"] == Decimal("19.99")  # โœ…

# Register Path support
kajson.UniversalJSONEncoder.register(
    Path, 
    lambda p: {"path": str(p)}
)
kajson.UniversalJSONDecoder.register(
    Path, 
    lambda d: Path(d["path"])
)

# Path objects now work too!
config = {"home": Path.home(), "config": Path("/etc/myapp/config.json")}
restored_config = kajson.loads(kajson.dumps(config))

Custom Classes with Hooks

Full example: ex_11_readme_custom_hooks.py

Add JSON support to your own classes:

from typing import Any, Dict
from typing_extensions import override

class Vector:
    def __init__(self, x: float, y: float):
        self.x = x
        self.y = y
    
    def __json_encode__(self):
        """Called by Kajson during serialization"""
        return {"x": self.x, "y": self.y}
    
    @classmethod
    def __json_decode__(cls, data: Dict[str, Any]):
        """Called by Kajson during deserialization"""
        return cls(data["x"], data["y"])
    
    @override
    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Vector):
            return False
        return self.x == other.x and self.y == other.y

# Works automatically!
vector = Vector(3.14, 2.71)
restored = kajson.loads(kajson.dumps(vector))
assert vector == restored

Working with Mixed Types

Full example: ex_12_readme_mixed_types.py

from datetime import datetime, timedelta
from typing import Any, Dict
from pydantic import BaseModel

class Task(BaseModel):
    name: str
    created_at: datetime
    duration: timedelta
    metadata: Dict[str, Any]

# Create mixed-type list
tasks = [
    Task(
        name="Data processing",
        created_at=datetime.now(),
        duration=timedelta(hours=2, minutes=30),
        metadata={"priority": "high", "cpu_cores": 8}
    ),
    {"raw_data": "Some plain dict"},
    datetime.now(),
    ["plain", "list", "items"],
]

# Kajson handles everything!
json_str = kajson.dumps(tasks)
restored_tasks = kajson.loads(json_str)

# Type checking shows proper reconstruction
assert isinstance(restored_tasks[0], Task)
assert isinstance(restored_tasks[0].duration, timedelta)
assert isinstance(restored_tasks[2], datetime)

๐Ÿ›ก๏ธ Error Handling

Full example: ex_13_readme_error_handling.py

Kajson provides clear error messages for validation issues:

from pydantic import BaseModel, Field

class Product(BaseModel):
    name: str
    price: float = Field(gt=0)  # Price must be positive

# Invalid data
json_str = '{"name": "Widget", "price": -10, "__class__": "Product", "__module__": "__main__"}'

try:
    product = kajson.loads(json_str)
except kajson.KajsonDecoderError as e:
    print(f"Validation failed: {e}")
    # Output: Validation failed: Could not instantiate pydantic BaseModel...

Dynamic Class Registry

Full example: ex_14_dynamic_class_registry.py

Kajson includes a powerful class registry for handling dynamically created classes that aren't available in standard module paths:

from kajson import kajson, kajson_manager
from kajson.kajson_manager import KajsonManager

# Simulate dynamic class creation (e.g., from network, workflow definition)
remote_class_definition = '''
from pydantic import BaseModel, Field

class RemoteTask(BaseModel):
    task_id: str
    name: str  
    priority: int = Field(default=1, ge=1, le=10)
'''

# Execute and create the class dynamically
remote_namespace = {}
exec(remote_class_definition, remote_namespace)
RemoteTask = remote_namespace["RemoteTask"]

# Set module to simulate it's not available locally
RemoteTask.__module__ = "remote.distributed.system"

# Create and serialize
task = RemoteTask(task_id="TASK_001", name="Process Data", priority=5)
json_str = kajson.dumps(task)

# Clear local definition (simulate distributed scenario)
del remote_namespace["RemoteTask"]

# Register in class registry for deserialization
registry = KajsonManager.get_class_registry()
registry.register_class(RemoteTask)

# Now deserialization works via class registry!
restored_task = kajson.loads(json_str)
assert restored_task.task_id == "TASK_001"

The Class Registry is essential for:

  • ๐ŸŒ Distributed systems - Classes defined across different services
  • โš™๏ธ Workflow orchestrators - Dynamic task definitions at runtime
  • ๐Ÿ”Œ Plugin systems - Runtime-loaded classes from plugins
  • ๐Ÿš€ Microservices - Exchanging complex object definitions
  • ๐Ÿญ Dynamic generation - Any runtime class creation scenarios

Pydantic Subclass Polymorphism

Full example: ex_15_pydantic_subclass_polymorphism.py

Kajson perfectly handles polymorphism with Pydantic models, preserving exact subclass types during serialization:

from pydantic import BaseModel

class Animal(BaseModel):
    name: str
    species: str

class Dog(Animal):
    breed: str
    is_good_boy: bool = True

class Pet(BaseModel):
    owner: str
    animal: Animal  # โ† Field declared as base class

# Create pet with subclass instance
pet = Pet(
    owner="Alice",
    animal=Dog(name="Buddy", species="Canis lupus", breed="Golden Retriever")  # โ† Actual subclass
)

# Serialize and deserialize
json_str = kajson.dumps(pet)
restored_pet = kajson.loads(json_str)

# Subclass type and attributes are perfectly preserved!
assert isinstance(restored_pet.animal, Dog)  # โœ… Still a Dog, not just Animal
assert restored_pet.animal.breed == "Golden Retriever"  # โœ… Subclass attributes preserved
assert restored_pet.animal.is_good_boy is True  # โœ… All fields intact

Perfect for:

  • ๐ŸŽญ Polymorphic APIs - Base class endpoints that handle multiple subclasses
  • ๐Ÿ—‚๏ธ Mixed collections - Lists of base class containing various subclasses
  • ๐Ÿ—๏ธ Plugin architectures - Runtime-loaded implementations of base interfaces
  • ๐Ÿ“Š Data modeling - Complex hierarchies with specialized behaviors

๐Ÿ”— Examples

For detailed examples and tutorials, visit: https://pipelex.github.io/kajson/pages/examples/

All code examples from this README are available as executable files in the examples/ directory:

Run any example with:

cd examples
python ex_01_basic_pydantic_serialization.py

Credits

This project is heavily based on the excellent work from unijson by Bastien Pietropaoli and distributed under the same license, Apache 2.0.

License

ยฉ 2025-2026 Evotis S.A.S. - Licensed under Apache 2.0

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

kajson-0.4.1.tar.gz (21.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

kajson-0.4.1-py3-none-any.whl (28.1 kB view details)

Uploaded Python 3

File details

Details for the file kajson-0.4.1.tar.gz.

File metadata

  • Download URL: kajson-0.4.1.tar.gz
  • Upload date:
  • Size: 21.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for kajson-0.4.1.tar.gz
Algorithm Hash digest
SHA256 0de2bc94153ab8cc7b3d72dd26701f7320f2dbbd6d19196b8ff2517ef6432265
MD5 a189ba88efdbd060f3cc201b814b50a4
BLAKE2b-256 cf58ced2096f15fcccd5cb6954f67f3bfaaf7e3f5c95ab85de9e03779cec2838

See more details on using hashes here.

Provenance

The following attestation bundles were made for kajson-0.4.1.tar.gz:

Publisher: publish-pypi.yml on Pipelex/kajson

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file kajson-0.4.1-py3-none-any.whl.

File metadata

  • Download URL: kajson-0.4.1-py3-none-any.whl
  • Upload date:
  • Size: 28.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for kajson-0.4.1-py3-none-any.whl
Algorithm Hash digest
SHA256 c46e6f705da7f93777c3d859a2ecc4e81ffa7f0ce9c4ef44ffa3b5a94764fe02
MD5 b13564136ea40d5bec315f230034d27e
BLAKE2b-256 02378687a8d3f7e983975cdab1d2e66f995df0c0c022c2905093c62b6b611eee

See more details on using hashes here.

Provenance

The following attestation bundles were made for kajson-0.4.1-py3-none-any.whl:

Publisher: publish-pypi.yml on Pipelex/kajson

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page