Skip to content

Custom Types

Learn how to extend Kajson to support any custom type through registration or built-in hooks.

Registration System

Basic Registration

The simplest way to add support for a custom type is through the registration system:

import kajson
from decimal import Decimal

# Register encoder
def encode_decimal(value: Decimal) -> dict:
    return {"__decimal__": str(value)}

# Register decoder
def decode_decimal(data: dict) -> Decimal:
    return Decimal(data["__decimal__"])

# Register both
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")
assert isinstance(restored["price"], Decimal)

Registration with Type Checking

For more robust implementations, include type checking:

import kajson
from pathlib import Path

def encode_path(path: Path) -> dict:
    return {
        "__path__": str(path),
        "is_absolute": path.is_absolute()
    }

def decode_path(data: dict) -> Path:
    if "__path__" not in data:
        raise ValueError("Invalid Path data")
    return Path(data["__path__"])

kajson.UniversalJSONEncoder.register(Path, encode_path)
kajson.UniversalJSONDecoder.register(Path, decode_path)

# Usage
config = {
    "project_root": Path("/home/user/project"),
    "config_file": Path("config/settings.json")
}

json_str = kajson.dumps(config)
restored = kajson.loads(json_str)

Custom Class Hooks

Using json_encode and json_decode

Classes can define their own serialization behavior:

import kajson
from typing import Tuple

class Color:
    def __init__(self, r: int, g: int, b: int, name: str = ""):
        self.r = r
        self.g = g
        self.b = b
        self.name = name

    def __json_encode__(self) -> dict:
        """Called by Kajson during serialization"""
        return {
            "rgb": (self.r, self.g, self.b),
            "name": self.name,
            "hex": f"#{self.r:02x}{self.g:02x}{self.b:02x}"
        }

    @classmethod
    def __json_decode__(cls, data: dict) -> "Color":
        """Called by Kajson during deserialization"""
        r, g, b = data["rgb"]
        return cls(r, g, b, data.get("name", ""))

    def __eq__(self, other):
        return (self.r, self.g, self.b, self.name) == (other.r, other.g, other.b, other.name)

# Works automatically
red = Color(255, 0, 0, "red")
json_str = kajson.dumps(red)
restored = kajson.loads(json_str)

assert red == restored

Complex Custom Types

import kajson
from typing import List, Optional
import numpy as np

class Matrix:
    def __init__(self, data: List[List[float]]):
        self.data = np.array(data)
        self.shape = self.data.shape

    def __json_encode__(self) -> dict:
        return {
            "data": self.data.tolist(),
            "shape": self.shape,
            "dtype": str(self.data.dtype)
        }

    @classmethod
    def __json_decode__(cls, data: dict) -> "Matrix":
        return cls(data["data"])

    def __eq__(self, other):
        return np.array_equal(self.data, other.data)

# Usage
matrix = Matrix([[1, 2, 3], [4, 5, 6]])
json_str = kajson.dumps(matrix)
restored = kajson.loads(json_str)

assert matrix == restored

Advanced Registration Patterns

Registering Multiple Types at Once

import kajson
from fractions import Fraction
from ipaddress import IPv4Address, IPv6Address

# Define encoders/decoders
type_handlers = {
    Fraction: {
        "encode": lambda f: {"num": f.numerator, "den": f.denominator},
        "decode": lambda d: Fraction(d["num"], d["den"])
    },
    IPv4Address: {
        "encode": lambda ip: {"ipv4": str(ip)},
        "decode": lambda d: IPv4Address(d["ipv4"])
    },
    IPv6Address: {
        "encode": lambda ip: {"ipv6": str(ip)},
        "decode": lambda d: IPv6Address(d["ipv6"])
    }
}

# Register all at once
for type_class, handlers in type_handlers.items():
    kajson.UniversalJSONEncoder.register(type_class, handlers["encode"])
    kajson.UniversalJSONDecoder.register(type_class, handlers["decode"])

# All types now work
data = {
    "fraction": Fraction(3, 4),
    "ipv4": IPv4Address("192.168.1.1"),
    "ipv6": IPv6Address("2001:db8::1")
}

json_str = kajson.dumps(data)
restored = kajson.loads(json_str)

Conditional Registration

import kajson
import platform

# Register platform-specific types
if platform.system() == "Windows":
    from pathlib import WindowsPath

    kajson.UniversalJSONEncoder.register(
        WindowsPath,
        lambda p: {"windows_path": str(p)}
    )
    kajson.UniversalJSONDecoder.register(
        WindowsPath,
        lambda d: WindowsPath(d["windows_path"])
    )

Working with Third-Party Libraries

NumPy Arrays

import kajson
import numpy as np

def encode_ndarray(arr: np.ndarray) -> dict:
    return {
        "data": arr.tolist(),
        "dtype": str(arr.dtype),
        "shape": arr.shape
    }

def decode_ndarray(data: dict) -> np.ndarray:
    arr = np.array(data["data"], dtype=data["dtype"])
    return arr.reshape(data["shape"])

kajson.UniversalJSONEncoder.register(np.ndarray, encode_ndarray)
kajson.UniversalJSONDecoder.register(np.ndarray, decode_ndarray)

# Usage
data = {
    "matrix": np.array([[1, 2], [3, 4]]),
    "vector": np.array([1.0, 2.0, 3.0])
}

json_str = kajson.dumps(data)
restored = kajson.loads(json_str)

assert np.array_equal(data["matrix"], restored["matrix"])

Pandas DataFrames

import kajson
import pandas as pd

def encode_dataframe(df: pd.DataFrame) -> dict:
    return {
        "data": df.to_dict(orient="records"),
        "columns": df.columns.tolist(),
        "index": df.index.tolist()
    }

def decode_dataframe(data: dict) -> pd.DataFrame:
    df = pd.DataFrame(data["data"])
    df.index = data["index"]
    return df[data["columns"]]  # Preserve column order

kajson.UniversalJSONEncoder.register(pd.DataFrame, encode_dataframe)
kajson.UniversalJSONDecoder.register(pd.DataFrame, decode_dataframe)

# Usage
df = pd.DataFrame({
    "name": ["Alice", "Bob", "Carol"],
    "age": [25, 30, 35],
    "city": ["NYC", "LA", "Chicago"]
})

json_str = kajson.dumps(df)
restored = kajson.loads(json_str)

assert df.equals(restored)

Best Practices

1. Use Unique Keys

Always use unique keys in your encoded data to avoid conflicts:

# Good - unique key unlikely to conflict
def encode_custom(value):
    return {"__mylib_custom__": value.data}

# Bad - generic key might conflict
def encode_custom(value):
    return {"data": value.data}

2. Include Version Information

For long-term compatibility, include version information:

def encode_complex_type(value):
    return {
        "__mytype__": {
            "version": 1,
            "data": value.serialize()
        }
    }

def decode_complex_type(data):
    version = data["__mytype__"]["version"]
    if version == 1:
        return MyType.deserialize(data["__mytype__"]["data"])
    else:
        raise ValueError(f"Unsupported version: {version}")

3. Handle Edge Cases

Always handle None and edge cases:

def encode_custom(value):
    if value is None:
        return None
    return {"__custom__": value.to_dict()}

def decode_custom(data):
    if data is None:
        return None
    if "__custom__" not in data:
        raise ValueError("Invalid custom data")
    return CustomType.from_dict(data["__custom__"])

4. Validate Input in Decoders

def decode_color(data: dict) -> Color:
    # Validate required fields
    if not all(key in data for key in ["r", "g", "b"]):
        raise ValueError("Missing required color components")

    # Validate ranges
    r, g, b = data["r"], data["g"], data["b"]
    if not all(0 <= c <= 255 for c in [r, g, b]):
        raise ValueError("Color values must be 0-255")

    return Color(r, g, b)

Creating Reusable Type Packages

You can create a package of custom type handlers:

# my_kajson_types.py
import kajson
from typing import Dict, Type, Callable, Tuple

class KajsonTypeRegistry:
    def __init__(self):
        self.types: Dict[Type, Tuple[Callable, Callable]] = {}

    def register(self, type_class: Type, encoder: Callable, decoder: Callable):
        self.types[type_class] = (encoder, decoder)

    def install(self):
        """Install all registered types into Kajson"""
        for type_class, (encoder, decoder) in self.types.items():
            kajson.UniversalJSONEncoder.register(type_class, encoder)
            kajson.UniversalJSONDecoder.register(type_class, decoder)

# Create registry
registry = KajsonTypeRegistry()

# Add types
from decimal import Decimal
registry.register(
    Decimal,
    lambda d: {"decimal": str(d)},
    lambda data: Decimal(data["decimal"])
)

# Usage in your app
from my_kajson_types import registry
registry.install()

Next Steps