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.
Say goodbye to type X is not JSON serializable!
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 serializableFull 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!- π Drop-in replacement - Same API as standard
jsonmodule - π 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
# Using pip
pip install kajson
# Using poetry
poetry add kajson
# Using uv (recommended)
uv pip install kajsonFull 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="[email protected]",
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()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- Python: 3.9+
- Pydantic: v2.x
- Dependencies: Minimal, only standard library + pydantic
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 objectsOr use Kajson's convenience functions directly:
import kajson
data = kajson.dumps(my_object)Kajson extends the standard JSON encoder/decoder by:
- Type Preservation: Adds
__class__and__module__metadata to JSON objects - Smart Decoding: Automatically reconstructs original Python objects
- Registry System: Allows registration of custom encoders/decoders
- Pydantic Integration: Special handling for Pydantic models and validation
- 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
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.
- 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)
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))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 == restoredFull 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)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...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
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 intactPerfect 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
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:
ex_01_basic_pydantic_serialization.py- Basic Pydantic model serializationex_02_nested_models_mixed_types.py- Complex nested models with datetime and timedeltaex_03_custom_classes_json_hooks.py- Point class using__json_encode__/__json_decode__hooksex_04_registering_custom_encoders.py- Custom type registrationex_05_mixed_types_lists.py- Lists containing different types (Task, datetime, dict, list, time)ex_06_error_handling_validation.py- Error handling and validationex_07_drop_in_replacement.py- Drop-in replacement for standard JSONex_08_readme_basic_usage.py- Why Kajson? (README example)ex_09_readme_complex_nested.py- Complex nested models (README example)ex_10_readme_custom_registration.py- Custom type registration (README example)ex_11_readme_custom_hooks.py- Custom hooks (README example)ex_12_readme_mixed_types.py- Mixed types (README example)ex_13_readme_error_handling.py- Error handling (README example)ex_14_dynamic_class_registry.py- Dynamic class registry for distributed systems and runtime class generationex_15_pydantic_subclass_polymorphism.py- Pydantic subclass polymorphism with perfect type preservation
Run any example with:
cd examples
python ex_01_basic_pydantic_serialization.pyThis project is heavily based on the excellent work from unijson by Bastien Pietropaoli and distributed under the same license, Apache 2.0.
Β© 2025 Evotis S.A.S. - Licensed under Apache 2.0