├── kajson
├── py.typed
├── singleton.py
├── exceptions.py
├── __init__.py
├── kajson_manager.py
├── class_registry_abstract.py
├── class_registry.py
└── kajson.py
├── tests
├── py.typed
├── unit
│ ├── kajson_api
│ │ ├── __init__.py
│ │ ├── test_timezone_encoder.py
│ │ ├── test_date_encoder_decoder.py
│ │ ├── test_timedelta_encoder.py
│ │ ├── test_time_encoder_decoder.py
│ │ ├── test_datetime_encoder_decoder.py
│ │ ├── test_encoder_registration.py
│ │ └── test_api_functions.py
│ ├── test_serde_pydantic_by_dict.py
│ ├── test_serde_union_discrim.py
│ └── test_enum_serialization.py
├── conftest.py
├── e2e
│ └── test_examples.py
└── test_data.py
├── docs
├── changelog.md
├── contributing.md
├── license.md
├── CODE_OF_CONDUCT.md
├── stylesheets
│ └── extra.css
├── pages
│ ├── installation.md
│ ├── credits.md
│ ├── api
│ │ └── manager.md
│ ├── quick-start.md
│ └── guide
│ │ ├── overview.md
│ │ └── basic-usage.md
└── index.md
├── examples
├── __init__.py
├── ex_01_basic_pydantic_serialization.py
├── ex_08_readme_basic_usage.py
├── ex_07_drop_in_replacement.py
├── ex_13_readme_error_handling.py
├── ex_06_error_handling_validation.py
├── ex_03_custom_classes_json_hooks.py
├── ex_11_readme_custom_hooks.py
├── ex_12_readme_mixed_types.py
├── ex_02_nested_models_mixed_types.py
├── ex_05_mixed_types_lists.py
├── ex_09_readme_complex_nested.py
├── ex_04_registering_custom_encoders.py
├── README.md
├── ex_10_readme_custom_registration.py
├── ex_14_dynamic_class_registry.py
├── ex_17_polymorphism_with_enums.py
└── ex_16_generic_models.py
├── .github
├── ISSUE_TEMPLATE
│ ├── config.yml
│ ├── general.yml
│ ├── feature_request.yml
│ └── bug_report.yml
├── workflows
│ ├── doc-check.yml
│ ├── deploy-docs.yml
│ ├── tests-check.yml
│ ├── changelog-check.yml
│ ├── cla.yml
│ ├── lint-check.yml
│ ├── guard-branches.yml
│ ├── publish-pypi.yml
│ └── version-check.yml
└── kajson_labels.json
├── .gitignore
├── LICENSING.md
├── .cursor
└── rules
│ ├── base_models.mdc
│ ├── tdd.mdc
│ ├── standards.mdc
│ ├── best_practices.mdc
│ └── pytest.mdc
├── .vscode
└── settings.json
├── mkdocs.yml
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CLA.md
├── CONTRIBUTING.md
└── pyproject.toml
/kajson/py.typed:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/py.typed:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/changelog.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Changelog
3 | ---
4 |
5 | --8<-- "CHANGELOG.md"
--------------------------------------------------------------------------------
/docs/contributing.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Contributing to Kajson
3 | ---
4 |
5 | --8<-- "CONTRIBUTING.md"
6 |
--------------------------------------------------------------------------------
/examples/__init__.py:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: © 2025 Evotis S.A.S.
2 | # SPDX-License-Identifier: Apache-2.0
3 |
--------------------------------------------------------------------------------
/tests/unit/kajson_api/__init__.py:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: © 2025 Evotis S.A.S.
2 | # SPDX-License-Identifier: Apache-2.0
3 |
--------------------------------------------------------------------------------
/docs/license.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: License
3 | hide:
4 | - feedback
5 | ---
6 |
7 | # License
8 |
9 | ```
10 | --8<-- "LICENSE"
11 | ```
12 |
--------------------------------------------------------------------------------
/docs/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Code of Conduct
3 | ---
4 |
5 | Please see the [Code of Conduct](https://github.com/Pipelex/kajson/blob/main/CODE_OF_CONDUCT.md) for our community guidelines.
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: 💬 Ask a question
4 | url: https://github.com/Pipelex/kajson/discussions
5 | about: "Please start a Discussion instead of filing a blank issue."
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # env
2 | .env
3 |
4 | # venv
5 | .venv
6 | venv/
7 |
8 | # reports
9 | reports/
10 |
11 | # pycache
12 | __pycache__
13 |
14 | # Python build artifacts
15 | build/*
16 | *.egg-info/
17 | dist/
18 | *.pyc
19 | .coverage
20 | site
21 |
--------------------------------------------------------------------------------
/LICENSING.md:
--------------------------------------------------------------------------------
1 | # Licensing
2 |
3 | All source code in this repository is released under the [Apache License 2.0](LICENSE).
4 |
5 | ## Third-party credit
6 |
7 | This project incorporates portions of [unijson](https://github.com/bpietropaoli/unijson) by Bastien Pietropaoli, also licensed under Apache 2.0.
8 |
9 | ---
10 |
11 | © 2025 Evotis S.A.S.
12 |
--------------------------------------------------------------------------------
/.cursor/rules/base_models.mdc:
--------------------------------------------------------------------------------
1 | ---
2 | description:
3 | globs:
4 | alwaysApply: true
5 | ---
6 | Rule to create BaseModels:
7 |
8 | - Respect Pydantic v2 standards
9 | - Keep models focused and single-purpose
10 | - Use descriptive field names
11 | - Use type hints for all fields
12 | - Document complex validations
13 | - Use Optional[] for nullable fields
14 | - Use Field(default_factory=...) for mutable defaults
15 |
--------------------------------------------------------------------------------
/kajson/singleton.py:
--------------------------------------------------------------------------------
1 | from typing import Any, ClassVar, Dict, Type
2 |
3 | from typing_extensions import override
4 |
5 |
6 | class MetaSingleton(type):
7 | """Simple implementation of a singleton using a metaclass."""
8 |
9 | instances: ClassVar[Dict[Type[Any], Any]] = {}
10 |
11 | @override
12 | def __call__(cls, *args: Any, **kwargs: Any) -> Any:
13 | if cls not in cls.instances: # pyright: ignore[reportUnnecessaryContains]
14 | cls.instances[cls] = super(MetaSingleton, cls).__call__(*args, **kwargs)
15 | return cls.instances[cls]
16 |
--------------------------------------------------------------------------------
/kajson/exceptions.py:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: © 2025 Evotis S.A.S.
2 | # SPDX-License-Identifier: Apache-2.0
3 |
4 |
5 | class RootException(Exception):
6 | def __init__(self, message: str):
7 | super().__init__(message)
8 | self.message = message
9 |
10 |
11 | class KajsonException(RootException):
12 | pass
13 |
14 |
15 | class ClassRegistryInheritanceError(KajsonException):
16 | pass
17 |
18 |
19 | class ClassRegistryNotFoundError(KajsonException):
20 | pass
21 |
22 |
23 | class KajsonDecoderError(KajsonException):
24 | pass
25 |
26 |
27 | class UnijsonEncoderError(KajsonException):
28 | pass
29 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "[python]": {
3 | "editor.defaultFormatter": "charliermarsh.ruff"
4 | },
5 | "ruff.configuration": "--config=pyproject.toml",
6 | "mypy.enabled": true,
7 | "mypy.runUsingActiveInterpreter": true,
8 | "search.exclude": {
9 | ".mypy_cache/*": true
10 | },
11 | "files.exclude": {
12 | "**/__pycache__": true,
13 | ".mypy_cache": true,
14 | ".pytest_cache": true,
15 | ".ruff_cache": true,
16 | },
17 | "python.testing.pytestArgs": [
18 | "tests"
19 | ],
20 | "python.testing.unittestEnabled": false,
21 | "python.testing.pytestEnabled": true
22 | }
--------------------------------------------------------------------------------
/docs/stylesheets/extra.css:
--------------------------------------------------------------------------------
1 | /* Custom styles for Kajson documentation */
2 |
3 | /* Adjust code block styling */
4 | .highlight pre {
5 | line-height: 1.4;
6 | }
7 |
8 | /* Better spacing for API reference */
9 | .doc-heading code {
10 | font-size: 0.95em;
11 | }
12 |
13 | /* Grid cards styling enhancement */
14 | .md-typeset .grid.cards> :is(ul, ol)>li> :first-child {
15 | margin-top: 0;
16 | }
17 |
18 | /* Improve admonition spacing */
19 | .md-typeset .admonition {
20 | margin: 1.5em 0;
21 | }
22 |
23 | /* Custom colors for Kajson branding */
24 | :root {
25 | --md-primary-fg-color--light: #00897b;
26 | --md-primary-fg-color--dark: #004d40;
27 | }
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | import kajson.kajson_manager
4 |
5 |
6 | @pytest.fixture(scope="function", autouse=True)
7 | def reset_kajson_manager_fixture():
8 | # Code to run before each test
9 | print("\n[magenta]Kajson setup[/magenta]")
10 | # Ensure clean state before creating instance
11 | kajson.kajson_manager.KajsonManager.teardown()
12 | try:
13 | kajson.kajson_manager.KajsonManager()
14 | except Exception as exc:
15 | pytest.exit(f"Critical Kajson setup error: {exc}")
16 | yield
17 | # Code to run after each test
18 | print("\n[magenta]Kajson teardown[/magenta]")
19 | kajson.kajson_manager.KajsonManager.teardown()
20 |
--------------------------------------------------------------------------------
/.github/workflows/doc-check.yml:
--------------------------------------------------------------------------------
1 | name: Documentation Check
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 | - dev
8 | - "release/v[0-9]+.[0-9]+.[0-9]+"
9 | paths:
10 | - 'docs/**'
11 | - 'mkdocs.yml'
12 |
13 | jobs:
14 | doc-check:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v4
18 |
19 | - name: Set up Python
20 | uses: actions/setup-python@v5
21 | with:
22 | python-version: '3.11'
23 | cache: 'pip'
24 |
25 | - name: Install mkdocs
26 | run: |
27 | python -m pip install --upgrade pip
28 | pip install mkdocs==1.6.1 mkdocs-material==9.6.14 mkdocs-glightbox==0.4.0 mkdocs-meta-manager==1.1.0
29 |
30 | - name: Check documentation build
31 | run: mkdocs build --strict
32 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-docs.yml:
--------------------------------------------------------------------------------
1 | name: Preview MkDocs on GitHub Pages
2 |
3 | on:
4 | push:
5 | branches: main
6 |
7 | permissions:
8 | contents: write
9 | pages: write
10 |
11 | jobs:
12 | preview:
13 | runs-on: ubuntu-latest
14 | env:
15 | VIRTUAL_ENV: ${{ github.workspace }}/.venv
16 | steps:
17 | - uses: actions/checkout@v4
18 |
19 | - uses: actions/setup-python@v5
20 | with:
21 | python-version: '3.x'
22 |
23 | - name: Check UV installation
24 | run: make check-uv
25 |
26 | - name: Verify UV installation
27 | run: uv --version
28 |
29 | - name: Install dependencies
30 | run: make install
31 |
32 | - name: Install docs dependencies
33 | run: uv pip install -e ".[docs]"
34 |
35 | - name: Deploy documentation
36 | run: make docs-deploy
37 |
--------------------------------------------------------------------------------
/tests/unit/kajson_api/test_timezone_encoder.py:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: © 2025 Evotis S.A.S.
2 | # SPDX-License-Identifier: Apache-2.0
3 |
4 | from zoneinfo import ZoneInfo
5 |
6 | from kajson import kajson
7 |
8 |
9 | class TestTimezoneEncoder:
10 | """Test cases for timezone encoding functions."""
11 |
12 | def test_json_encode_timezone(self) -> None:
13 | """Test json_encode_timezone function (covers line 95)."""
14 | timezone = ZoneInfo("America/New_York")
15 | result = kajson.json_encode_timezone(timezone)
16 |
17 | expected = {"zone": "America/New_York"}
18 | assert result == expected
19 |
20 | def test_json_encode_timezone_utc(self) -> None:
21 | """Test json_encode_timezone with UTC timezone."""
22 | timezone = ZoneInfo("UTC")
23 | result = kajson.json_encode_timezone(timezone)
24 |
25 | expected = {"zone": "UTC"}
26 | assert result == expected
27 |
--------------------------------------------------------------------------------
/.cursor/rules/tdd.mdc:
--------------------------------------------------------------------------------
1 | ---
2 | description:
3 | globs:
4 | alwaysApply: false
5 | ---
6 | # Test-Driven Development Guide
7 |
8 | This document outlines our test-driven development (TDD) process and the tools available for testing.
9 |
10 | ## TDD Cycle
11 |
12 | 1. **Write a Test First**
13 | [pytest.mdc](mdc:.cursor/rules/pytest.mdc)
14 |
15 | 2. **Write the Code**
16 | - Implement the minimum amount of code needed to pass the test
17 | - Follow the project's coding standards
18 | - Keep it simple - don't write more than needed
19 |
20 | 3. **Run Linting and Type Checking**
21 | [standards.mdc](mdc:.cursor/rules/standards.mdc)
22 |
23 | 4. **Refactor if needed**
24 | If the code needs refactoring, with the best practices [best_practices.mdc](mdc:.cursor/rules/best_practices.mdc)
25 |
26 | 5. **Validate tests**
27 |
28 | Remember: The key to TDD is writing the test first and letting it drive your implementation. Always run the full test suite and quality checks before considering a feature complete.
29 |
--------------------------------------------------------------------------------
/kajson/__init__.py:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: © 2025 Evotis S.A.S.
2 | # SPDX-License-Identifier: Apache-2.0
3 |
4 | # Main API functions (drop-in replacement for json module)
5 | # Exception classes
6 | from kajson.exceptions import (
7 | ClassRegistryInheritanceError,
8 | ClassRegistryNotFoundError,
9 | KajsonDecoderError,
10 | KajsonException,
11 | UnijsonEncoderError,
12 | )
13 | from kajson.json_decoder import UniversalJSONDecoder
14 |
15 | # Encoder and Decoder classes for custom type registration
16 | from kajson.json_encoder import UniversalJSONEncoder
17 | from kajson.kajson import dump, dumps, load, loads
18 |
19 | # Export all main symbols
20 | __all__ = [
21 | # Main API functions
22 | "dumps",
23 | "dump",
24 | "loads",
25 | "load",
26 | # Encoder/Decoder classes
27 | "UniversalJSONEncoder",
28 | "UniversalJSONDecoder",
29 | # Exception classes
30 | "KajsonException",
31 | "KajsonDecoderError",
32 | "ClassRegistryInheritanceError",
33 | "ClassRegistryNotFoundError",
34 | "UnijsonEncoderError",
35 | ]
36 |
--------------------------------------------------------------------------------
/.github/workflows/tests-check.yml:
--------------------------------------------------------------------------------
1 | name: Tests check
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 | - dev
8 | - "release/v[0-9]+.[0-9]+.[0-9]+"
9 |
10 | jobs:
11 | matrix-test:
12 | name: Tests check
13 | runs-on: ubuntu-latest
14 | strategy:
15 | fail-fast: false
16 | matrix:
17 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
18 | permissions:
19 | contents: read
20 | id-token: write
21 | env:
22 | VIRTUAL_ENV: ${{ github.workspace }}/.venv
23 | ENV: dev
24 | steps:
25 | - uses: actions/checkout@v4
26 |
27 | - name: Set up Python ${{ matrix.python-version }}
28 | uses: actions/setup-python@v4
29 | with:
30 | python-version: ${{ matrix.python-version }}
31 |
32 | - name: Check UV installation
33 | run: make check-uv
34 |
35 | - name: Verify UV installation
36 | run: uv --version
37 |
38 | - name: Install dependencies
39 | run: PYTHON_VERSION=${{ matrix.python-version }} make install
40 |
41 | - name: Run tests
42 | run: make gha-tests
43 |
--------------------------------------------------------------------------------
/docs/pages/installation.md:
--------------------------------------------------------------------------------
1 | # Installation
2 |
3 | Kajson requires Python 3.9 or higher and can be installed using your favorite package manager.
4 |
5 | ## Requirements
6 |
7 | - Python ≥ 3.9
8 | - Pydantic v2 (installed automatically)
9 |
10 | ## Installation Methods
11 |
12 | ### Using pip
13 |
14 | ```bash
15 | pip install kajson
16 | ```
17 |
18 | ### Using poetry
19 |
20 | ```bash
21 | poetry add kajson
22 | ```
23 |
24 | ### Using uv (recommended)
25 |
26 | ```bash
27 | uv pip install kajson
28 | ```
29 |
30 | ## Development Installation
31 |
32 | If you want to contribute to Kajson or install from source:
33 |
34 | ```bash
35 | # Clone the repository
36 | git clone https://github.com/Pipelex/kajson.git
37 | cd kajson
38 |
39 | # Install with development dependencies
40 | make install
41 | ```
42 |
43 | ## Verify Installation
44 |
45 | After installation, you can verify that Kajson is properly installed:
46 |
47 | ```python
48 | import kajson
49 |
50 | # Check version
51 | print(kajson.__version__)
52 |
53 | # Test basic functionality
54 | data = {"message": "Hello, Kajson!"}
55 | json_str = kajson.dumps(data)
56 | print(json_str)
57 | ```
58 |
59 | ## Next Steps
60 |
61 | Once installed, check out the [Quick Start Guide](quick-start.md) to begin using Kajson in your projects.
62 |
--------------------------------------------------------------------------------
/kajson/kajson_manager.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from typing_extensions import Self
4 |
5 | from kajson.class_registry import ClassRegistry
6 | from kajson.class_registry_abstract import ClassRegistryAbstract
7 | from kajson.singleton import MetaSingleton
8 |
9 | KAJSON_LOGGER_CHANNEL_NAME = "kajson"
10 |
11 |
12 | class KajsonManager(metaclass=MetaSingleton):
13 | """A singleton class for managing kajson operations."""
14 |
15 | def __init__(
16 | self,
17 | logger_channel_name: Optional[str] = None,
18 | class_registry: Optional[ClassRegistryAbstract] = None,
19 | ) -> None:
20 | self.logger_channel_name = logger_channel_name or KAJSON_LOGGER_CHANNEL_NAME
21 | self._class_registry = class_registry or ClassRegistry()
22 |
23 | @classmethod
24 | def get_instance(cls) -> Self:
25 | """Get the singleton instance. This will create one if it doesn't exist."""
26 | return cls()
27 |
28 | @classmethod
29 | def teardown(cls) -> None:
30 | """Destroy the singleton instance."""
31 | if cls in MetaSingleton.instances:
32 | del MetaSingleton.instances[cls]
33 |
34 | @classmethod
35 | def get_class_registry(cls) -> ClassRegistryAbstract:
36 | return cls.get_instance()._class_registry
37 |
--------------------------------------------------------------------------------
/.github/workflows/changelog-check.yml:
--------------------------------------------------------------------------------
1 | name: Changelog Version Check
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 | types: [opened, synchronize, reopened]
8 |
9 | jobs:
10 | check-changelog:
11 | runs-on: ubuntu-latest
12 | if: startsWith(github.head_ref, 'release/v') # Only run on release branches
13 | steps:
14 | - uses: actions/checkout@v4
15 |
16 | - name: Check Changelog Version
17 | run: |
18 | # Get the current branch name and extract version
19 | BRANCH_NAME=${GITHUB_HEAD_REF}
20 |
21 | if [[ $BRANCH_NAME =~ ^release/v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
22 | VERSION="${BASH_REMATCH[1]}"
23 | echo "Checking release branch v$VERSION against changelog..."
24 |
25 | # Look for the version in the changelog
26 | if ! grep -q "## \[v$VERSION\] -" CHANGELOG.md; then
27 | echo "❌ Error: No changelog entry found for version v$VERSION"
28 | echo "The following versions are in the changelog:"
29 | grep -E "^## \[v[0-9]+\.[0-9]+\.[0-9]+\]" CHANGELOG.md
30 | echo "Please add a changelog entry for v$VERSION before merging this release to main"
31 | exit 1
32 | else
33 | echo "✅ Changelog entry found for version v$VERSION"
34 | fi
35 | else
36 | echo "❌ Error: Branch name $BRANCH_NAME does not match expected format 'release/vX.Y.Z'"
37 | exit 1
38 | fi
39 |
--------------------------------------------------------------------------------
/examples/ex_01_basic_pydantic_serialization.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Basic Pydantic Model Serialization Example
4 |
5 | This example demonstrates how Kajson seamlessly handles Pydantic models
6 | with datetime fields, providing perfect reconstruction of objects.
7 | """
8 |
9 | from datetime import datetime
10 |
11 | from pydantic import BaseModel
12 |
13 | from kajson import kajson, kajson_manager
14 |
15 |
16 | class User(BaseModel):
17 | name: str
18 | email: str
19 | created_at: datetime
20 |
21 |
22 | def main():
23 | print("=== Basic Pydantic Model Serialization Example ===\n")
24 |
25 | # Create and serialize
26 | user = User(name="Alice", email="alice@example.com", created_at=datetime.now())
27 |
28 | print(f"Original user: {user}")
29 | print(f"User type: {type(user)}")
30 | print(f"Created at type: {type(user.created_at)}")
31 |
32 | # Serialize to JSON
33 | json_str = kajson.dumps(user, indent=2)
34 | print(f"\nSerialized JSON:\n{json_str}")
35 |
36 | # Deserialize back
37 | restored_user = kajson.loads(json_str)
38 | print(f"\nRestored user: {restored_user}")
39 | print(f"Restored type: {type(restored_user)}")
40 | print(f"Restored created_at type: {type(restored_user.created_at)}")
41 |
42 | # Verify perfect reconstruction
43 | assert user == restored_user # ✅ Perfect reconstruction!
44 | print("\n✅ Perfect reconstruction! Original and restored users are equal.")
45 |
46 |
47 | if __name__ == "__main__":
48 | kajson_manager.KajsonManager()
49 | main()
50 |
--------------------------------------------------------------------------------
/examples/ex_08_readme_basic_usage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | README Basic Usage Example - Why Kajson?
4 |
5 | This example demonstrates the problem with standard JSON and how Kajson solves it.
6 | This is the main example from the README.md showing why Kajson is needed.
7 | """
8 |
9 | import json
10 | from datetime import datetime
11 |
12 | from pydantic import BaseModel
13 |
14 | from kajson import kajson, kajson_manager
15 |
16 |
17 | class User(BaseModel):
18 | name: str
19 | created_at: datetime
20 |
21 |
22 | def main():
23 | print("=== README Basic Usage Example - Why Kajson? ===\n")
24 |
25 | user = User(name="Alice", created_at=datetime.now())
26 | print(f"User object: {user}")
27 | print(f"User type: {type(user)}")
28 |
29 | # ❌ Standard json fails
30 | print("\n--- Trying with standard json module ---")
31 | try:
32 | json_str = json.dumps(user)
33 | print(f"Standard json result: {json_str}")
34 | except TypeError as e:
35 | print(f"❌ Standard json fails: {e}")
36 |
37 | # ✅ Just works!
38 | print("\n--- Trying with kajson ---")
39 | json_str = kajson.dumps(user)
40 | print(f"✅ Kajson works: {json_str}")
41 |
42 | restored_user = kajson.loads(json_str)
43 | print(f"Restored user: {restored_user}")
44 | print(f"Restored type: {type(restored_user)}")
45 |
46 | assert user == restored_user # Perfect reconstruction!
47 | print("✅ Perfect reconstruction!")
48 |
49 |
50 | if __name__ == "__main__":
51 | kajson_manager.KajsonManager()
52 | main()
53 |
--------------------------------------------------------------------------------
/examples/ex_07_drop_in_replacement.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Drop-in Replacement Usage Example
4 |
5 | This example demonstrates how Kajson can be used as a drop-in replacement
6 | for Python's standard json module, handling complex types seamlessly.
7 | """
8 |
9 | # Simply change your import
10 | from datetime import datetime
11 |
12 | import kajson as json # Instead of: import json
13 | from kajson import kajson_manager
14 |
15 |
16 | def main():
17 | print("=== Drop-in Replacement Usage Example ===\n")
18 |
19 | print("Using kajson as a drop-in replacement for standard json...")
20 |
21 | # All your existing code works!
22 | data = {"user": "Alice", "logged_in": datetime.now()}
23 | print(f"Original data: {data}")
24 | print(f"Logged in type: {type(data['logged_in'])}")
25 |
26 | json_str = json.dumps(data) # Works with datetime!
27 | print(f"\nSerialized JSON: {json_str}")
28 |
29 | restored = json.loads(json_str)
30 | print(f"\nRestored data: {restored}")
31 | print(f"Restored logged_in type: {type(restored['logged_in'])}")
32 |
33 | print("\n✅ Standard json module replaced seamlessly!")
34 |
35 | # Or use kajson directly
36 | import kajson
37 |
38 | print("\nAlternatively, using kajson directly:")
39 | json_str2 = kajson.dumps(data)
40 | restored2 = kajson.loads(json_str2)
41 |
42 | print(f"Direct kajson result: {restored2}")
43 | assert restored == restored2
44 | print("✅ Both approaches work identically!")
45 |
46 |
47 | if __name__ == "__main__":
48 | kajson_manager.KajsonManager()
49 | main()
50 |
--------------------------------------------------------------------------------
/.github/workflows/cla.yml:
--------------------------------------------------------------------------------
1 | name: "CLA Assistant bot"
2 | on:
3 | issue_comment:
4 | types: [created]
5 | pull_request_target:
6 | types: [opened, closed, synchronize]
7 |
8 | permissions:
9 | actions: write
10 | contents: read
11 | pull-requests: write
12 | statuses: write
13 |
14 | jobs:
15 | CLAAssistant:
16 | runs-on: ubuntu-latest
17 | steps:
18 | - name: Get GitHub App token
19 | id: app-token
20 | uses: actions/create-github-app-token@v1
21 | with:
22 | app-id: ${{ secrets.CLA_GH_APP_ID }}
23 | private-key: ${{ secrets.CLA_GH_APP_PRIVATE_KEY }}
24 | owner: Pipelex
25 | repositories: |
26 | cla-signatures
27 | kajson
28 |
29 | - name: "CLA Assistant"
30 | if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
31 | uses: contributor-assistant/github-action@v2.6.1
32 | env:
33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
34 | PERSONAL_ACCESS_TOKEN: ${{ steps.app-token.outputs.token }}
35 | with:
36 | path-to-signatures: "signatures/version1/cla.json"
37 | path-to-document: "https://github.com/Pipelex/kajson/blob/main/CLA.md"
38 | branch: main
39 | allowlist: lchoquel,thomashebrard,bot*
40 | remote-organization-name: Pipelex
41 | remote-repository-name: cla-signatures
42 | signed-commit-message: "$contributorName has signed the CLA in $owner/$repo#$pullRequestNo"
43 |
--------------------------------------------------------------------------------
/examples/ex_13_readme_error_handling.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | README Error Handling Example
4 |
5 | This example demonstrates how Kajson provides clear error messages
6 | for validation issues when working with Pydantic models.
7 | """
8 |
9 | from pydantic import BaseModel, Field
10 |
11 | from kajson import kajson, kajson_manager
12 |
13 |
14 | class Product(BaseModel):
15 | name: str
16 | price: float = Field(gt=0) # Price must be positive
17 |
18 |
19 | def main():
20 | print("=== README Error Handling Example ===\n")
21 |
22 | # First, show that valid data works
23 | valid_product = Product(name="Valid Widget", price=25.99)
24 | print(f"Valid product: {valid_product}")
25 |
26 | valid_json = kajson.dumps(valid_product)
27 | restored_valid = kajson.loads(valid_json)
28 | print(f"Valid product restored: {restored_valid}")
29 | print("✅ Valid data works perfectly!\n")
30 |
31 | # Invalid data
32 | json_str = '{"name": "Widget", "price": -10, "__class__": "Product", "__module__": "__main__"}'
33 | print(f"Attempting to load invalid JSON: {json_str}")
34 |
35 | try:
36 | product = kajson.loads(json_str)
37 | print(f"❌ This should not happen: {product}")
38 | except kajson.KajsonDecoderError:
39 | print("✅ Validation failed as expected!")
40 | print(" Kajson caught the Pydantic validation error:")
41 | print(" → Price must be greater than 0 (got -10)")
42 | print(" This prevents invalid data from being silently accepted!")
43 |
44 |
45 | if __name__ == "__main__":
46 | kajson_manager.KajsonManager()
47 | main()
48 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/general.yml:
--------------------------------------------------------------------------------
1 | name: "📝 General issue"
2 | description: "Use this for questions, docs tweaks, refactors, or anything that isn't a Bug or Feature request."
3 | type: Task
4 | labels:
5 | - status:needs-triage
6 |
7 | body:
8 | - type: markdown
9 | attributes:
10 | value: |
11 | **Thanks for opening an issue!**
12 | This form is for ideas or tasks that don't fit the Bug or Feature templates.
13 |
14 | - type: checkboxes
15 | id: confirmations
16 | attributes:
17 | label: "Before submitting"
18 | options:
19 | - label: "I've checked [open issues](issues?q=is%3Aissue%20state%3Aopen) and found no similar item"
20 | required: true
21 | - label: "It's not really a bug report or a feature request"
22 | required: false
23 |
24 | - type: textarea
25 | id: summary
26 | attributes:
27 | label: "What would you like to discuss or change?"
28 | placeholder: |
29 | A clear, concise description of the question, enhancement, refactor, or documentation update.
30 | validations:
31 | required: true
32 |
33 | - type: textarea
34 | id: context
35 | attributes:
36 | label: "Relevant context (optional)"
37 | placeholder: |
38 | Links, screenshots, or any additional details that will help us understand.
39 | validations:
40 | required: false
41 |
42 | - type: dropdown
43 | id: contribution
44 | attributes:
45 | label: Would you like to help drive or implement this?
46 | options:
47 | - "Not at this time"
48 | - "Yes, I'd like to contribute"
49 | validations:
50 | required: false
51 |
--------------------------------------------------------------------------------
/tests/unit/kajson_api/test_date_encoder_decoder.py:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: © 2025 Evotis S.A.S.
2 | # SPDX-License-Identifier: Apache-2.0
3 |
4 | import datetime
5 |
6 | from kajson import kajson
7 |
8 |
9 | class TestDateEncoderDecoder:
10 | """Test cases for date encoding and decoding functions."""
11 |
12 | def test_json_encode_date(self) -> None:
13 | """Test json_encode_date function."""
14 | test_date = datetime.date(2023, 12, 25)
15 | result = kajson.json_encode_date(test_date)
16 |
17 | expected = {"date": "2023-12-25"}
18 | assert result == expected
19 |
20 | def test_json_decode_date(self) -> None:
21 | """Test json_decode_date function (covers line 111)."""
22 | test_dict = {"date": "2023-12-25"}
23 | result = kajson.json_decode_date(test_dict)
24 |
25 | expected = datetime.date(2023, 12, 25)
26 | assert result == expected
27 |
28 | def test_json_decode_date_leap_year(self) -> None:
29 | """Test json_decode_date with leap year date (covers line 117)."""
30 | test_dict = {"date": "2024-02-29"}
31 | result = kajson.json_decode_date(test_dict)
32 |
33 | expected = datetime.date(2024, 2, 29)
34 | assert result == expected
35 |
36 | def test_date_roundtrip_serialization(self) -> None:
37 | """Test complete date serialization roundtrip."""
38 | original_date = datetime.date(2023, 6, 15)
39 |
40 | # Encode
41 | json_str = kajson.dumps(original_date)
42 |
43 | # Decode
44 | decoded_date = kajson.loads(json_str)
45 |
46 | assert decoded_date == original_date
47 | assert isinstance(decoded_date, datetime.date)
48 |
--------------------------------------------------------------------------------
/.cursor/rules/standards.mdc:
--------------------------------------------------------------------------------
1 | ---
2 | description:
3 | globs:
4 | alwaysApply: true
5 | ---
6 | # Coding Standards
7 |
8 | This document outlines the coding standards and quality control procedures that must be followed when contributing to this project.
9 |
10 | ## Style
11 |
12 | Always use type hints. Use the types with Uppercase first letter for types like Dict[], List[] etc.
13 |
14 | ## Code Quality Checks
15 |
16 | ### Linting and Type Checking
17 |
18 | Before finalizing a task, you must run the following command to check for linting issues, type errors, and code quality problems:
19 |
20 | ```bash
21 | make check
22 | ```
23 |
24 | This command runs multiple code quality tools:
25 | - Pyright: Static type checking
26 | - Ruff: Fast Python linter
27 | - Mypy: Static type checker
28 |
29 | Always fix any issues reported by these tools before proceeding.
30 |
31 | ### Running Tests
32 |
33 | We have several make commands for running tests:
34 |
35 | 1. `make tp`: Runs all tests with these markers:
36 | ```
37 | (dry_runnable or not (inference or llm or imgg or ocr)) and not (needs_output or pipelex_api)
38 | ```
39 | Use this for quick test runs that don't require LLM or image generation.
40 |
41 | 2. To run specific tests:
42 | ```bash
43 | make tp TEST=TestClassName
44 | # or
45 | make tp TEST=test_function_name
46 | ```
47 | It matches names, so `TEST=test_function_name` is going to run all test with the function name that STARTS with `test_function_name`.
48 |
49 | ## Important Project Directories
50 |
51 | ### Tests Directory
52 | - All tests are located in the `tests/` directory
53 |
54 | ### Documentation Directory
55 | - All documentation is located in the `docs/` directory
56 |
--------------------------------------------------------------------------------
/.cursor/rules/best_practices.mdc:
--------------------------------------------------------------------------------
1 | ---
2 | description:
3 | globs:
4 | alwaysApply: false
5 | ---
6 | # Best Practices Guide
7 |
8 | This document outlines the core best practices and patterns used in our codebase.
9 |
10 | ## Type Hints
11 |
12 | **Always Use Type Hints**
13 | - Every function parameter must be typed
14 | - Every function return must be typed
15 | - Use type hints for all variables where type is not obvious
16 |
17 | ## Factory Pattern
18 |
19 | 1. **Use Factory Pattern for Object Creation**
20 | - Create factories when dealing with multiple implementations
21 |
22 | ## Documentation
23 |
24 | 1. **Docstring Format**
25 | - Quick description of the function/class
26 | - List args and their types
27 | - Document return values
28 | - Example:
29 | ```python
30 | def process_image(image_path: str, size: Tuple[int, int]) -> bytes:
31 | """Process and resize an image.
32 |
33 | Args:
34 | image_path: Path to the source image
35 | size: Tuple of (width, height) for resizing
36 |
37 | Returns:
38 | Processed image as bytes
39 | """
40 | pass
41 | ```
42 |
43 | 2. **Class Documentation**
44 | - Document class purpose and behavior
45 | - Include examples if complex
46 | ```python
47 | class ImageProcessor:
48 | """Handles image processing operations.
49 |
50 | Provides methods for resizing, converting, and optimizing images.
51 | """
52 | ```
53 |
54 | ## Custom Exceptions
55 |
56 | 1. **Graceful Error Handling**
57 | - Use try/except blocks with specific exceptions and convert Python exceptions or third-party exceptions to custom ones only whne you can brign extra details and add sense to the reported exception
58 |
59 |
--------------------------------------------------------------------------------
/examples/ex_06_error_handling_validation.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Error Handling with Validation Example
4 |
5 | This example shows how Kajson handles validation errors when deserializing
6 | JSON data that doesn't meet Pydantic model constraints.
7 | """
8 |
9 | from pydantic import BaseModel, Field
10 |
11 | from kajson import kajson, kajson_manager
12 |
13 |
14 | class Product(BaseModel):
15 | name: str
16 | price: float = Field(gt=0) # Must be positive
17 |
18 |
19 | def main():
20 | print("=== Error Handling with Validation Example ===\n")
21 |
22 | # Valid data works fine
23 | product = Product(name="Widget", price=19.99)
24 | print(f"Valid product: {product}")
25 |
26 | json_str = kajson.dumps(product)
27 | print(f"Serialized JSON: {json_str}")
28 |
29 | restored = kajson.loads(json_str)
30 | print(f"Restored product: {restored}")
31 | print("✅ Valid data works perfectly!\n")
32 |
33 | # Invalid data in JSON
34 | invalid_json = """
35 | {
36 | "name": "Widget",
37 | "price": -10,
38 | "__class__": "Product",
39 | "__module__": "__main__"
40 | }
41 | """
42 |
43 | print("Attempting to load invalid JSON with negative price...")
44 | print(f"Invalid JSON: {invalid_json.strip()}")
45 |
46 | try:
47 | kajson.loads(invalid_json)
48 | print("❌ This should not happen - validation should fail!")
49 | except kajson.KajsonDecoderError:
50 | print("✅ Validation failed as expected!")
51 | print(" Kajson properly caught the Pydantic validation error:")
52 | print(" → Price must be greater than 0 (got -10)")
53 | print(" This ensures data integrity when deserializing!")
54 |
55 |
56 | if __name__ == "__main__":
57 | kajson_manager.KajsonManager()
58 | main()
59 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------
1 | name: "✨ Feature request"
2 | description: "Suggest an idea or improvement for the library"
3 | type: Feature
4 | labels:
5 | - status:needs-triage
6 |
7 | body:
8 | - type: markdown
9 | attributes:
10 | value: |
11 | **Thanks for contributing an idea!**
12 | Please fill in the sections below so we can understand and prioritise your request.
13 |
14 | - type: checkboxes
15 | id: confirmations
16 | attributes:
17 | label: "Before submitting"
18 | options:
19 | - label: "I've searched [open issues](issues?q=is%3Aissue%20state%3Aopen%20type%3AFeature) and found no similar request"
20 | required: true
21 | - label: "I'm willing to start a discussion or contribute code"
22 | required: false
23 |
24 | - type: textarea
25 | id: problem
26 | attributes:
27 | label: "Problem / motivation"
28 | placeholder: "What problem does this feature solve? Who is affected and why?"
29 | validations:
30 | required: true
31 |
32 | - type: textarea
33 | id: proposal
34 | attributes:
35 | label: "Proposed solution"
36 | placeholder: "Describe the feature you'd like to see."
37 | validations:
38 | required: false
39 |
40 | - type: textarea
41 | id: alternatives
42 | attributes:
43 | label: "Alternatives considered"
44 | placeholder: "Any work-arounds you've tried or other approaches you considered."
45 | validations:
46 | required: false
47 |
48 | - type: dropdown
49 | id: contribution
50 | attributes:
51 | label: Would you like to help implement this feature?
52 | options:
53 | - "Not at this time"
54 | - "Yes, I'd like to contribute"
55 | validations:
56 | required: false
57 |
--------------------------------------------------------------------------------
/kajson/class_registry_abstract.py:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: © 2025 Evotis S.A.S.
2 | # SPDX-License-Identifier: Apache-2.0
3 |
4 | from abc import ABC, abstractmethod
5 | from typing import Any, Dict, List, Optional, Type
6 |
7 | from pydantic import BaseModel
8 |
9 |
10 | class ClassRegistryAbstract(ABC):
11 | @abstractmethod
12 | def setup(self) -> None:
13 | pass
14 |
15 | @abstractmethod
16 | def teardown(self) -> None:
17 | pass
18 |
19 | @abstractmethod
20 | def register_class(
21 | self,
22 | class_type: Type[Any],
23 | name: Optional[str] = None,
24 | should_warn_if_already_registered: bool = True,
25 | ) -> None:
26 | pass
27 |
28 | @abstractmethod
29 | def unregister_class(self, class_type: Type[Any]) -> None:
30 | pass
31 |
32 | @abstractmethod
33 | def unregister_class_by_name(self, name: str) -> None:
34 | pass
35 |
36 | @abstractmethod
37 | def register_classes_dict(self, classes: Dict[str, Type[Any]]) -> None:
38 | pass
39 |
40 | @abstractmethod
41 | def register_classes(self, classes: List[Type[Any]]) -> None:
42 | pass
43 |
44 | @abstractmethod
45 | def get_class(self, name: str) -> Optional[Type[Any]]:
46 | pass
47 |
48 | @abstractmethod
49 | def get_required_class(self, name: str) -> Type[Any]:
50 | pass
51 |
52 | @abstractmethod
53 | def get_required_subclass(self, name: str, base_class: Type[Any]) -> Type[Any]:
54 | pass
55 |
56 | @abstractmethod
57 | def get_required_base_model(self, name: str) -> Type[BaseModel]:
58 | pass
59 |
60 | @abstractmethod
61 | def has_class(self, name: str) -> bool:
62 | pass
63 |
64 | @abstractmethod
65 | def has_subclass(self, name: str, base_class: Type[Any]) -> bool:
66 | pass
67 |
--------------------------------------------------------------------------------
/examples/ex_03_custom_classes_json_hooks.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Custom Classes with JSON Hooks Example
4 |
5 | This example demonstrates how to add JSON serialization support to custom classes
6 | using the __json_encode__ and __json_decode__ methods.
7 | """
8 |
9 | from typing import Any, Dict
10 |
11 | from typing_extensions import override
12 |
13 | from kajson import kajson, kajson_manager
14 |
15 |
16 | class Point:
17 | def __init__(self, x: float, y: float):
18 | self.x = x
19 | self.y = y
20 |
21 | def __json_encode__(self):
22 | """Called during serialization"""
23 | return {"x": self.x, "y": self.y}
24 |
25 | @classmethod
26 | def __json_decode__(cls, data: Dict[str, Any]):
27 | """Called during deserialization"""
28 | return cls(data["x"], data["y"])
29 |
30 | @override
31 | def __eq__(self, other: object) -> bool:
32 | if not isinstance(other, Point):
33 | return False
34 | return self.x == other.x and self.y == other.y
35 |
36 | @override
37 | def __repr__(self) -> str:
38 | return f"Point(x={self.x}, y={self.y})"
39 |
40 |
41 | def main():
42 | print("=== Custom Classes with JSON Hooks Example ===\n")
43 |
44 | # Use it directly
45 | point = Point(3.14, 2.71)
46 | print(f"Original point: {point}")
47 | print(f"Point type: {type(point)}")
48 |
49 | json_str = kajson.dumps(point)
50 | print(f"\nSerialized JSON: {json_str}")
51 |
52 | restored = kajson.loads(json_str)
53 | print(f"\nRestored point: {restored}")
54 | print(f"Restored type: {type(restored)}")
55 |
56 | # Verify equality
57 | assert point == restored
58 | print("\n✅ Perfect reconstruction! Original and restored points are equal.")
59 |
60 |
61 | if __name__ == "__main__":
62 | kajson_manager.KajsonManager()
63 | main()
64 |
--------------------------------------------------------------------------------
/examples/ex_11_readme_custom_hooks.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | README Custom Classes with Hooks Example
4 |
5 | This example shows how to add JSON serialization support to custom classes
6 | using the __json_encode__ and __json_decode__ hook methods.
7 | """
8 |
9 | from typing import Any, Dict
10 |
11 | from typing_extensions import override
12 |
13 | import kajson
14 | from kajson import kajson_manager
15 |
16 |
17 | class Vector:
18 | def __init__(self, x: float, y: float):
19 | self.x = x
20 | self.y = y
21 |
22 | def __json_encode__(self):
23 | """Called by Kajson during serialization"""
24 | return {"x": self.x, "y": self.y}
25 |
26 | @classmethod
27 | def __json_decode__(cls, data: Dict[str, Any]):
28 | """Called by Kajson during deserialization"""
29 | return cls(data["x"], data["y"])
30 |
31 | @override
32 | def __eq__(self, other: object) -> bool:
33 | if not isinstance(other, Vector):
34 | return False
35 | return self.x == other.x and self.y == other.y
36 |
37 | @override
38 | def __repr__(self) -> str:
39 | return f"Vector(x={self.x}, y={self.y})"
40 |
41 |
42 | def main():
43 | print("=== README Custom Classes with Hooks Example ===\n")
44 |
45 | # Works automatically!
46 | vector = Vector(3.14, 2.71)
47 | print(f"Original vector: {vector}")
48 | print(f"Vector type: {type(vector)}")
49 |
50 | json_str = kajson.dumps(vector)
51 | print(f"\nSerialized JSON: {json_str}")
52 |
53 | restored = kajson.loads(json_str)
54 | print(f"\nRestored vector: {restored}")
55 | print(f"Restored type: {type(restored)}")
56 |
57 | assert vector == restored
58 | print("✅ Vectors are equal - custom hooks work perfectly!")
59 |
60 |
61 | if __name__ == "__main__":
62 | kajson_manager.KajsonManager()
63 | main()
64 |
--------------------------------------------------------------------------------
/docs/pages/credits.md:
--------------------------------------------------------------------------------
1 | # Credits
2 |
3 | ## Core Team
4 |
5 | Kajson is developed and maintained by the team at **Pipelex** which is also behind the open-source language for repeatable AI workflows: [Pipelex](https://github.com/Pipelex/pipelex).
6 |
7 | ## Original Work
8 |
9 | This project is heavily based on the excellent work from [unijson](https://github.com/bpietropaoli/unijson) by Bastien Pietropaoli. We are grateful for the foundation that unijson provided, which allowed us to build Kajson with enhanced features and Pydantic v2 support.
10 |
11 | ## Contributors
12 |
13 | We thank all contributors who have helped improve Kajson through bug reports, feature suggestions, and code contributions. Special thanks to:
14 |
15 | - The Pydantic team for creating an amazing data validation library
16 | - The Python community for the great ecosystem
17 |
18 | ## Open Source Libraries
19 |
20 | Kajson stands on the shoulders of giants. We'd like to acknowledge the following projects:
21 |
22 | - [Python](https://www.python.org/) - The programming language that makes it all possible
23 | - [Pydantic](https://pydantic-docs.helpmanual.io/) - Data validation using Python type annotations
24 | - [pytest](https://pytest.org/) - Testing framework
25 | - [MkDocs](https://www.mkdocs.org/) - Documentation framework
26 | - [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/) - Beautiful documentation theme
27 |
28 | ## License
29 |
30 | Kajson is distributed under the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0), the same license as the original unijson project.
31 |
32 | ## Support
33 |
34 | For questions, bug reports, or feature requests:
35 |
36 | - Open an issue on [GitHub](https://github.com/Pipelex/kajson/issues)
37 | - Join our community on [Discord](https://go.pipelex.com/discord)
38 | - Check out [Pipelex](https://github.com/Pipelex/pipelex) for AI workflow automation using Kajson
39 |
--------------------------------------------------------------------------------
/examples/ex_12_readme_mixed_types.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | README Working with Mixed Types Example
4 |
5 | This example demonstrates how Kajson handles complex data structures
6 | containing mixed types including Pydantic models, datetime objects, and plain Python types.
7 | """
8 |
9 | from datetime import datetime, timedelta
10 | from typing import Any, Dict
11 |
12 | from pydantic import BaseModel
13 |
14 | import kajson
15 | from kajson import kajson_manager
16 |
17 |
18 | class Task(BaseModel):
19 | name: str
20 | created_at: datetime
21 | duration: timedelta
22 | metadata: Dict[str, Any]
23 |
24 |
25 | def main():
26 | print("=== README Working with Mixed Types Example ===\n")
27 |
28 | # Create mixed-type list
29 | tasks = [
30 | Task(
31 | name="Data processing", created_at=datetime.now(), duration=timedelta(hours=2, minutes=30), metadata={"priority": "high", "cpu_cores": 8}
32 | ),
33 | {"raw_data": "Some plain dict"},
34 | datetime.now(),
35 | ["plain", "list", "items"],
36 | ]
37 |
38 | print("Original mixed-type list:")
39 | for i, task in enumerate(tasks):
40 | print(f" [{i}] {task} (type: {type(task).__name__})")
41 |
42 | # Kajson handles everything!
43 | json_str = kajson.dumps(tasks)
44 | print(f"\nSerialized JSON (truncated): {json_str[:300]}...")
45 |
46 | restored_tasks = kajson.loads(json_str)
47 |
48 | print("\nRestored mixed-type list:")
49 | for i, task in enumerate(restored_tasks):
50 | print(f" [{i}] {task} (type: {type(task).__name__})")
51 |
52 | # Type checking shows proper reconstruction
53 | assert isinstance(restored_tasks[0], Task)
54 | assert isinstance(restored_tasks[0].duration, timedelta)
55 | assert isinstance(restored_tasks[2], datetime)
56 |
57 | print("\n✅ All types perfectly preserved!")
58 | print(f"✅ Task duration: {restored_tasks[0].duration}")
59 | print(f"✅ Task metadata: {restored_tasks[0].metadata}")
60 |
61 |
62 | if __name__ == "__main__":
63 | kajson_manager.KajsonManager()
64 | main()
65 |
--------------------------------------------------------------------------------
/examples/ex_02_nested_models_mixed_types.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Nested Models with Mixed Types Example
4 |
5 | This example shows how Kajson handles complex nested Pydantic models
6 | with various types including datetime, timedelta, and lists.
7 | """
8 |
9 | from datetime import datetime, timedelta
10 | from typing import List
11 |
12 | from pydantic import BaseModel
13 |
14 | from kajson import kajson, kajson_manager
15 |
16 |
17 | class Comment(BaseModel):
18 | author: str
19 | text: str
20 | posted_at: datetime
21 |
22 |
23 | class BlogPost(BaseModel):
24 | title: str
25 | content: str
26 | published_at: datetime
27 | read_time: timedelta
28 | comments: List[Comment]
29 |
30 |
31 | def main():
32 | print("=== Nested Models with Mixed Types Example ===\n")
33 |
34 | # Create complex nested structure
35 | post = BlogPost(
36 | title="Kajson Makes JSON Easy",
37 | content="No more 'not JSON serializable' errors!",
38 | published_at=datetime.now(),
39 | read_time=timedelta(minutes=5),
40 | comments=[
41 | Comment(author="Bob", text="Great post!", posted_at=datetime.now()),
42 | Comment(author="Carol", text="Very helpful", posted_at=datetime.now()),
43 | ],
44 | )
45 |
46 | print(f"Original post: {post}")
47 | print(f"Post type: {type(post)}")
48 | print(f"Read time type: {type(post.read_time)}")
49 | print(f"First comment type: {type(post.comments[0])}")
50 |
51 | # Works seamlessly!
52 | json_str = kajson.dumps(post)
53 | print(f"\nSerialized JSON (truncated): {json_str[:200]}...")
54 |
55 | restored = kajson.loads(json_str)
56 | print(f"\nRestored post: {restored}")
57 | print(f"Restored type: {type(restored)}")
58 | print(f"Restored read_time type: {type(restored.read_time)}")
59 | print(f"Restored first comment type: {type(restored.comments[0])}")
60 |
61 | # Verify reconstruction
62 | assert post == restored
63 | print("\n✅ Perfect reconstruction! Original and restored posts are equal.")
64 |
65 |
66 | if __name__ == "__main__":
67 | kajson_manager.KajsonManager()
68 | main()
69 |
--------------------------------------------------------------------------------
/examples/ex_05_mixed_types_lists.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Working with Lists of Mixed Types Example
4 |
5 | This example demonstrates how Kajson handles lists containing different types
6 | including Pydantic models, datetime objects, enums, and plain Python structures.
7 | """
8 |
9 | from datetime import date, datetime, time
10 | from enum import Enum
11 |
12 | from pydantic import BaseModel
13 |
14 | from kajson import kajson, kajson_manager
15 |
16 |
17 | class Status(Enum):
18 | PENDING = "pending"
19 | IN_PROGRESS = "in_progress"
20 | COMPLETED = "completed"
21 |
22 |
23 | class Task(BaseModel):
24 | name: str
25 | due_date: date
26 |
27 |
28 | def main():
29 | print("=== Working with Lists of Mixed Types Example ===\n")
30 |
31 | # Mix different types in one list
32 | mixed_data = [
33 | Task(name="Review PR", due_date=date.today()),
34 | datetime.now(),
35 | {"plain": "dict"},
36 | ["plain", "list"],
37 | time(14, 30),
38 | Status.IN_PROGRESS,
39 | ]
40 |
41 | print("Original mixed data:")
42 | for i, item in enumerate(mixed_data):
43 | print(f" [{i}] {item} (type: {type(item).__name__})")
44 |
45 | # Kajson handles everything
46 | json_str = kajson.dumps(mixed_data)
47 | print(f"\nSerialized JSON (truncated): {json_str[:200]}...")
48 |
49 | restored = kajson.loads(json_str)
50 |
51 | print("\nRestored mixed data:")
52 | for i, item in enumerate(restored):
53 | print(f" [{i}] {item} (type: {type(item).__name__})")
54 |
55 | # Types are preserved
56 | assert isinstance(restored[0], Task)
57 | assert isinstance(restored[1], datetime)
58 | assert isinstance(restored[4], time)
59 | assert isinstance(restored[5], Status)
60 |
61 | print("\n✅ All types preserved correctly!")
62 | print(f"✅ Task object: {restored[0].name} due on {restored[0].due_date}")
63 | print(f"✅ Datetime object: {restored[1]}")
64 | print(f"✅ Time object: {restored[4]}")
65 | print(f"✅ Enum object: {restored[5]} (value: {restored[5].value})")
66 |
67 |
68 | if __name__ == "__main__":
69 | kajson_manager.KajsonManager()
70 | main()
71 |
--------------------------------------------------------------------------------
/.github/workflows/lint-check.yml:
--------------------------------------------------------------------------------
1 | name: Lint check
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 | - dev
8 | - "release/v[0-9]+.[0-9]+.[0-9]+"
9 |
10 | jobs:
11 | # --------------------------------------------------------------------------
12 | # 1. Matrix job — one runner *per* Python version
13 | # --------------------------------------------------------------------------
14 | lint:
15 | name: Lint (${{ matrix.python-version }})
16 | runs-on: ubuntu-latest
17 | strategy:
18 | fail-fast: false
19 | matrix:
20 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
21 | env:
22 | VIRTUAL_ENV: ${{ github.workspace }}/.venv
23 |
24 | steps:
25 | - uses: actions/checkout@v4
26 |
27 | - name: Set up Python ${{ matrix.python-version }}
28 | uses: actions/setup-python@v4
29 | with:
30 | python-version: ${{ matrix.python-version }}
31 |
32 | - name: Check UV installation
33 | run: make check-uv
34 |
35 | - name: Verify UV installation
36 | run: uv --version
37 |
38 | - name: Install dependencies
39 | run: PYTHON_VERSION=${{ matrix.python-version }} make install
40 |
41 | - name: Run ruff format merge check
42 | run: make merge-check-ruff-format
43 |
44 | - name: Run ruff lint merge check
45 | run: make merge-check-ruff-lint
46 |
47 | - name: Run pyright merge check
48 | run: make merge-check-pyright
49 |
50 | - name: Run mypy merge check
51 | run: make merge-check-mypy
52 |
53 | # --------------------------------------------------------------------------
54 | # 2. Aggregator job — the *single* required status check
55 | # --------------------------------------------------------------------------
56 | lint-all:
57 | name: Lint (all versions)
58 | runs-on: ubuntu-latest
59 | needs: lint # wait for every matrix leg
60 | if: always() # run even if one leg already failed
61 |
62 | steps:
63 | - name: Fail if any matrix leg failed
64 | run: |
65 | if [ "${{ needs.lint.result }}" != "success" ]; then
66 | echo "::error::At least one Python version failed linting."
67 | exit 1
68 | fi
69 | echo "✅ All Python versions passed lint checks."
70 |
--------------------------------------------------------------------------------
/tests/e2e/test_examples.py:
--------------------------------------------------------------------------------
1 | class TestExamples:
2 | def test_01_basic_pydantic_serialization(self):
3 | import examples.ex_01_basic_pydantic_serialization # noqa: F401
4 |
5 | def test_02_nested_models_mixed_types(self):
6 | import examples.ex_02_nested_models_mixed_types # noqa: F401
7 |
8 | def test_03_custom_classes_json_hooks(self):
9 | import examples.ex_03_custom_classes_json_hooks # noqa: F401
10 |
11 | def test_04_registering_custom_encoders(self):
12 | import examples.ex_04_registering_custom_encoders # noqa: F401
13 |
14 | def test_05_mixed_types_lists(self):
15 | import examples.ex_05_mixed_types_lists # noqa: F401
16 |
17 | def test_06_error_handling_validation(self):
18 | import examples.ex_06_error_handling_validation # noqa: F401
19 |
20 | def test_07_drop_in_replacement(self):
21 | import examples.ex_07_drop_in_replacement # noqa: F401
22 |
23 | def test_08_readme_basic_usage(self):
24 | import examples.ex_08_readme_basic_usage # noqa: F401
25 |
26 | def test_09_readme_complex_nested(self):
27 | import examples.ex_09_readme_complex_nested # noqa: F401
28 |
29 | def test_10_readme_custom_registration(self):
30 | import examples.ex_10_readme_custom_registration # noqa: F401
31 |
32 | def test_11_readme_custom_hooks(self):
33 | import examples.ex_11_readme_custom_hooks # noqa: F401
34 |
35 | def test_12_readme_mixed_types(self):
36 | import examples.ex_12_readme_mixed_types # noqa: F401
37 |
38 | def test_13_readme_error_handling(self):
39 | import examples.ex_13_readme_error_handling # noqa: F401
40 |
41 | def test_14_dynamic_class_registry(self):
42 | import examples.ex_14_dynamic_class_registry # noqa: F401
43 |
44 | def test_15_pydantic_subclass_polymorphism(self):
45 | """Test that Pydantic subclass polymorphism works correctly."""
46 | import examples.ex_15_pydantic_subclass_polymorphism # noqa: F401
47 |
48 | def test_16_generic_models(self):
49 | """Test that generic Pydantic models work correctly."""
50 | import examples.ex_16_generic_models # noqa: F401
51 |
52 | def test_17_polymorphism_with_enums(self):
53 | """Test that polymorphism with enums works correctly."""
54 | import examples.ex_17_polymorphism_with_enums # noqa: F401
55 |
--------------------------------------------------------------------------------
/examples/ex_09_readme_complex_nested.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | README Complex Nested Models Example
4 |
5 | This example shows how Kajson handles complex nested Pydantic models
6 | with multiple levels of nesting and various field types.
7 | """
8 |
9 | from datetime import datetime
10 | from typing import Any, Dict, List
11 |
12 | from pydantic import BaseModel
13 |
14 | import kajson
15 | from kajson import kajson_manager
16 |
17 |
18 | class Comment(BaseModel):
19 | author: str
20 | content: str
21 | created_at: datetime
22 |
23 |
24 | class BlogPost(BaseModel):
25 | title: str
26 | content: str
27 | published_at: datetime
28 | comments: List[Comment]
29 | metadata: Dict[str, Any]
30 |
31 |
32 | def main():
33 | print("=== README Complex Nested Models Example ===\n")
34 |
35 | # Create complex nested structure
36 | post = BlogPost(
37 | title="Introducing Kajson",
38 | content="A powerful JSON library...",
39 | published_at=datetime.now(),
40 | comments=[
41 | Comment(author="Alice", content="Great post!", created_at=datetime.now()),
42 | Comment(author="Bob", content="Very helpful", created_at=datetime.now()),
43 | ],
44 | metadata={"views": 1000, "likes": 50},
45 | )
46 |
47 | print(f"Original post: {post}")
48 | print(f"Post type: {type(post)}")
49 | print(f"Number of comments: {len(post.comments)}")
50 | print(f"First comment type: {type(post.comments[0])}")
51 |
52 | # Serialize and deserialize - it just works!
53 | json_str = kajson.dumps(post)
54 | print(f"\nSerialized JSON (first 300 chars): {json_str[:300]}...")
55 |
56 | restored_post = kajson.loads(json_str)
57 | print(f"\nRestored post title: {restored_post.title}")
58 | print(f"Restored post type: {type(restored_post)}")
59 | print(f"Restored comments count: {len(restored_post.comments)}")
60 |
61 | # All nested objects are perfectly preserved
62 | assert isinstance(restored_post.comments[0], Comment)
63 | assert restored_post.comments[0].created_at.year == datetime.now().year
64 | print("✅ All nested objects are perfectly preserved!")
65 | print(f"✅ First comment: {restored_post.comments[0].author} - {restored_post.comments[0].content}")
66 |
67 |
68 | if __name__ == "__main__":
69 | kajson_manager.KajsonManager()
70 | main()
71 |
--------------------------------------------------------------------------------
/.github/kajson_labels.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "priority:P0",
4 | "color": "B60205",
5 | "description": "Critical — stop the line"
6 | },
7 | {
8 | "name": "priority:P1",
9 | "color": "D93F0B",
10 | "description": "High priority"
11 | },
12 | {
13 | "name": "priority:P2",
14 | "color": "E36209",
15 | "description": "Normal priority"
16 | },
17 | {
18 | "name": "priority:P3",
19 | "color": "FBCA04",
20 | "description": "Low priority"
21 | },
22 | {
23 | "name": "status:needs-triage",
24 | "color": "A2BFFC",
25 | "description": "Awaiting triage"
26 | },
27 | {
28 | "name": "status:needs-info",
29 | "color": "1E90FF",
30 | "description": "Needs more information"
31 | },
32 | {
33 | "name": "status:blocked",
34 | "color": "004385",
35 | "description": "Work is blocked"
36 | },
37 | {
38 | "name": "status:in-progress",
39 | "color": "0969DA",
40 | "description": "Currently being worked on"
41 | },
42 | {
43 | "name": "status:review",
44 | "color": "6CA4F8",
45 | "description": "Awaiting code review"
46 | },
47 | {
48 | "name": "status:done",
49 | "color": "14866D",
50 | "description": "Completed / merged"
51 | },
52 | {
53 | "name": "status:duplicate",
54 | "color": "6F42C1",
55 | "description": "May close as soon as it's verified."
56 | },
57 | {
58 | "name": "status:not-planned",
59 | "color": "6A737D",
60 | "description": "Signals a likely won't-fix before formal closure."
61 | },
62 | {
63 | "name": "status:invalid",
64 | "color": "E4E669",
65 | "description": "Needs more info or out of scope."
66 | },
67 | {
68 | "name": "area:core",
69 | "color": "005630",
70 | "description": "Core library logic"
71 | },
72 | {
73 | "name": "area:examples",
74 | "color": "005630",
75 | "description": "Examples"
76 | },
77 | {
78 | "name": "area:docs",
79 | "color": "28A745",
80 | "description": "Documentation"
81 | },
82 | {
83 | "name": "area:tests",
84 | "color": "7FDA9E",
85 | "description": "Unit / integration / e2e tests"
86 | },
87 | {
88 | "name": "good first issue",
89 | "color": "FFD33D",
90 | "description": "Great for new contributors"
91 | },
92 | {
93 | "name": "help wanted",
94 | "color": "FFEA7F",
95 | "description": "Maintainers would love help"
96 | }
97 | ]
--------------------------------------------------------------------------------
/examples/ex_04_registering_custom_encoders.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Registering Custom Type Encoders Example
4 |
5 | This example shows how to register custom encoders and decoders for types
6 | like Decimal and Path that aren't supported by default.
7 | """
8 |
9 | from decimal import Decimal
10 | from pathlib import Path
11 | from typing import Any, Dict
12 |
13 | from kajson import kajson, kajson_manager
14 |
15 |
16 | def main():
17 | print("=== Registering Custom Type Encoders Example ===\n")
18 |
19 | # Register Decimal support
20 | kajson.UniversalJSONEncoder.register(Decimal, lambda d: {"decimal": str(d)})
21 | kajson.UniversalJSONDecoder.register(Decimal, lambda data: Decimal(data["decimal"]))
22 |
23 | # Register Path support - need to handle both abstract Path and concrete types
24 | def encode_path(p: Path) -> Dict[str, Any]:
25 | return {"path": str(p)}
26 |
27 | def decode_path(data: Dict[str, Any]) -> Path:
28 | return Path(data["path"])
29 |
30 | kajson.UniversalJSONEncoder.register(Path, encode_path)
31 | kajson.UniversalJSONDecoder.register(Path, decode_path)
32 |
33 | # Also register for the concrete Path type (PosixPath/WindowsPath)
34 | concrete_path_type = type(Path())
35 | if concrete_path_type != Path:
36 | kajson.UniversalJSONEncoder.register(concrete_path_type, encode_path)
37 | kajson.UniversalJSONDecoder.register(concrete_path_type, decode_path)
38 |
39 | print("✅ Registered custom encoders for Decimal and Path")
40 |
41 | # Now they work!
42 | data = {"price": Decimal("19.99"), "config_path": Path("/etc/app/config.json")}
43 |
44 | print(f"\nOriginal data: {data}")
45 | print(f"Price type: {type(data['price'])}")
46 | print(f"Config path type: {type(data['config_path'])}")
47 |
48 | json_str = kajson.dumps(data)
49 | print(f"\nSerialized JSON: {json_str}")
50 |
51 | restored = kajson.loads(json_str)
52 | print(f"\nRestored data: {restored}")
53 | print(f"Restored price type: {type(restored['price'])}")
54 | print(f"Restored config path type: {type(restored['config_path'])}")
55 |
56 | # Verify types and values
57 | assert restored["price"] == Decimal("19.99")
58 | assert isinstance(restored["config_path"], Path)
59 | print("\n✅ Custom types work perfectly!")
60 |
61 |
62 | if __name__ == "__main__":
63 | kajson_manager.KajsonManager()
64 | main()
65 |
--------------------------------------------------------------------------------
/tests/unit/kajson_api/test_timedelta_encoder.py:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: © 2025 Evotis S.A.S.
2 | # SPDX-License-Identifier: Apache-2.0
3 |
4 | import datetime
5 |
6 | from kajson import kajson
7 |
8 |
9 | class TestTimedeltaEncoder:
10 | """Test cases for timedelta encoding function."""
11 |
12 | def test_json_encode_timedelta_positive(self) -> None:
13 | """Test json_encode_timedelta with positive timedelta (covers line 190)."""
14 | test_timedelta = datetime.timedelta(days=5, hours=3, minutes=30, seconds=45)
15 | result = kajson.json_encode_timedelta(test_timedelta)
16 |
17 | expected_seconds = test_timedelta.total_seconds()
18 | expected = {"seconds": expected_seconds}
19 | assert result == expected
20 |
21 | def test_json_encode_timedelta_negative(self) -> None:
22 | """Test json_encode_timedelta with negative timedelta."""
23 | test_timedelta = datetime.timedelta(days=-2, hours=-5)
24 | result = kajson.json_encode_timedelta(test_timedelta)
25 |
26 | expected_seconds = test_timedelta.total_seconds()
27 | expected = {"seconds": expected_seconds}
28 | assert result == expected
29 | assert result["seconds"] < 0
30 |
31 | def test_json_encode_timedelta_zero(self) -> None:
32 | """Test json_encode_timedelta with zero timedelta."""
33 | test_timedelta = datetime.timedelta()
34 | result = kajson.json_encode_timedelta(test_timedelta)
35 |
36 | expected = {"seconds": 0.0}
37 | assert result == expected
38 |
39 | def test_json_encode_timedelta_microseconds(self) -> None:
40 | """Test json_encode_timedelta with microseconds."""
41 | test_timedelta = datetime.timedelta(seconds=1, microseconds=500000)
42 | result = kajson.json_encode_timedelta(test_timedelta)
43 |
44 | expected = {"seconds": 1.5}
45 | assert result == expected
46 |
47 | def test_timedelta_roundtrip_serialization(self) -> None:
48 | """Test complete timedelta serialization roundtrip."""
49 | original_timedelta = datetime.timedelta(days=3, hours=2, minutes=30, seconds=45, microseconds=123456)
50 |
51 | # Encode
52 | json_str = kajson.dumps(original_timedelta)
53 |
54 | # Decode (should be handled by automatic constructor calling)
55 | decoded_timedelta = kajson.loads(json_str)
56 |
57 | assert decoded_timedelta == original_timedelta
58 | assert isinstance(decoded_timedelta, datetime.timedelta)
59 |
--------------------------------------------------------------------------------
/tests/unit/test_serde_pydantic_by_dict.py:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: © 2025 Evotis S.A.S.
2 | # SPDX-License-Identifier: Apache-2.0
3 |
4 | import json
5 | import logging
6 | from typing import Any, Dict
7 |
8 | import pytest
9 | from pydantic import BaseModel
10 |
11 | from tests.test_data import SerDeTestCases
12 |
13 |
14 | class TestSerDePydanticByDict:
15 | @pytest.mark.parametrize("test_obj, test_obj_dict, test_obj_dict_typed, test_obj_json_str4", SerDeTestCases.PYDANTIC_FULL_CHECKS)
16 | def test_dump_dict_validate(
17 | self,
18 | test_obj: BaseModel,
19 | test_obj_dict: Dict[str, Any],
20 | test_obj_dict_typed: Dict[str, Any],
21 | test_obj_json_str4: str,
22 | ):
23 | # Serialize the model to a dictionary
24 | deserialized_dict = test_obj.model_dump()
25 | logging.info("Serialized")
26 | logging.info(deserialized_dict)
27 | assert deserialized_dict == test_obj_dict_typed
28 |
29 | # Validate the dictionary back to a model
30 | the_class = type(test_obj)
31 | deserialized = the_class.model_validate(deserialized_dict)
32 | logging.info("Deserialized")
33 | logging.info(deserialized)
34 |
35 | assert test_obj == deserialized
36 |
37 | @pytest.mark.parametrize("test_obj, test_obj_dict, test_obj_dict_typed, test_obj_json_str4", SerDeTestCases.PYDANTIC_FULL_CHECKS)
38 | def test_serde_dump_json_load_validate(
39 | self,
40 | test_obj: BaseModel,
41 | test_obj_dict: Dict[str, Any],
42 | test_obj_dict_typed: Dict[str, Any],
43 | test_obj_json_str4: str,
44 | ):
45 | # Serialize the model to a json string
46 | serialized_str = test_obj.model_dump_json()
47 | logging.info("Serialized JSON")
48 | logging.info(serialized_str)
49 |
50 | # Deserialize the json string back to a dictionary
51 | deserialized_dict = json.loads(serialized_str)
52 | assert deserialized_dict == test_obj_dict
53 | logging.info("Deserialized to dict")
54 | logging.info(deserialized_dict)
55 | assert deserialized_dict["created_at"] == "2023-01-01T12:00:00"
56 | assert deserialized_dict["updated_at"] == "2023-01-02T12:13:25"
57 |
58 | # Validate the dictionary back to a model
59 | the_class = type(test_obj)
60 | validated_model = the_class.model_validate(deserialized_dict)
61 | logging.info("Validated model")
62 | logging.info(validated_model)
63 |
64 | assert test_obj == validated_model
65 |
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | # Kajson Examples
2 |
3 | This directory contains runnable Python examples demonstrating all the features of Kajson. Each example is self-contained and can be run independently.
4 |
5 | ## Examples from Documentation
6 |
7 | ### Basic Usage Examples
8 | - **01_basic_pydantic_serialization.py** - Basic Pydantic model with datetime serialization
9 | - **08_readme_basic_usage.py** - Shows the problem with standard JSON and how Kajson solves it
10 |
11 | ### Complex Nested Models
12 | - **02_nested_models_mixed_types.py** - BlogPost with Comment models, timedelta support
13 | - **09_readme_complex_nested.py** - Complex nested structures with metadata
14 |
15 | ### Custom Type Support
16 | - **03_custom_classes_json_hooks.py** - Point class using `__json_encode__`/`__json_decode__` hooks
17 | - **11_readme_custom_hooks.py** - Vector class with custom JSON hooks
18 | - **04_registering_custom_encoders.py** - Register Decimal and Path type encoders
19 | - **10_readme_custom_registration.py** - Advanced custom type registration
20 |
21 | ### Mixed Type Handling
22 | - **05_mixed_types_lists.py** - Lists containing different types (Task, datetime, dict, list, time)
23 | - **12_readme_mixed_types.py** - Complex mixed-type data structures with timedelta
24 |
25 | ### Error Handling
26 | - **06_error_handling_validation.py** - Pydantic validation error handling
27 | - **13_readme_error_handling.py** - Clear error messages for validation failures
28 |
29 | ### Integration Examples
30 | - **07_drop_in_replacement.py** - Using Kajson as a drop-in replacement for standard json
31 |
32 | ## Development and Testing Examples
33 |
34 | - **14_dynamic_class_registry.py** - When and why to use the class registry for dynamic classes
35 | - **15_pydantic_subclass_polymorphism.py** - Polymorphism with Pydantic models: subclass preservation during serialization
36 |
37 | ## Running the Examples
38 |
39 | Each example can be run independently:
40 |
41 | ```bash
42 | # Run a specific example
43 | python examples/01_basic_pydantic_serialization.py
44 |
45 | # Or run from the examples directory
46 | cd examples
47 | python 01_basic_pydantic_serialization.py
48 | ```
49 |
50 | ## Example Output
51 |
52 | Each example includes:
53 | - ✅ Clear success indicators
54 | - 🔍 Type checking demonstrations
55 | - 📊 Before/after comparisons
56 | - 💡 Explanatory comments
57 |
58 | ## Requirements
59 |
60 | All examples require:
61 | - Python 3.9+
62 | - kajson (installed in development mode)
63 | - pydantic (for BaseModel examples)
64 |
65 | The examples use only standard library imports plus kajson and pydantic, making them easy to run and understand.
--------------------------------------------------------------------------------
/.github/workflows/guard-branches.yml:
--------------------------------------------------------------------------------
1 | name: Guard branch flow
2 | on:
3 | pull_request_target:
4 | types: [opened, edited, synchronize, reopened]
5 |
6 | jobs:
7 | # ───────────────────────────────────────────────────────────────
8 | # 1) Only release/vX.Y.Z → main
9 | # ───────────────────────────────────────────────────────────────
10 | gate-main:
11 | if: github.event.pull_request.base.ref == 'main'
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Verify source branch is a Release
15 | env:
16 | HEAD: ${{ github.event.pull_request.head.ref }}
17 | run: |
18 | echo "PR → main from $HEAD"
19 | if [[ ! "$HEAD" =~ ^release\/v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
20 | echo "::error::Only release/vX.Y.Z branches may merge into main."
21 | exit 1
22 | fi
23 |
24 | # ───────────────────────────────────────────────────────────────
25 | # 2) Only work-branches → release/vX.Y.Z
26 | # ───────────────────────────────────────────────────────────────
27 | gate-release:
28 | if: startsWith(github.event.pull_request.base.ref, 'release/v') || github.event.pull_request.base.ref == 'dev'
29 | runs-on: ubuntu-latest
30 | steps:
31 | - name: Verify source branch uses allowed prefix
32 | env:
33 | HEAD: ${{ github.event.pull_request.head.ref }}
34 | run: |
35 | echo "PR → ${{ github.event.pull_request.base.ref }} from $HEAD"
36 | if [[ "$HEAD" == "dev" ]]; then
37 | exit 0
38 | fi
39 | if [[ ! "$HEAD" =~ ^(fix|feature|refactor|chore|docs|ci-cd|changelog)\/[A-Za-z0-9._\/\<\>\=\-]+$ ]]; then
40 | echo "::error::Branch must start with fix/, feature/, refactor/, chore/, docs/, or ci-cd/."
41 | exit 1
42 | fi
43 |
44 | # ───────────────────────────────────────────────────────────────
45 | # 3) Prevent forks from editing your workflows
46 | # ───────────────────────────────────────────────────────────────
47 | protect-workflows:
48 | runs-on: ubuntu-latest
49 | # only block non-maintainers
50 | if: github.event.pull_request.author_association == 'CONTRIBUTOR'
51 | steps:
52 | - name: Checkout code
53 | uses: actions/checkout@v4
54 | with:
55 | fetch-depth: 0
56 | - name: Detect workflow changes
57 | run: |
58 | git fetch origin "${{ github.event.pull_request.base.ref }}" --depth=1
59 | CHANGED=$(git diff --name-only FETCH_HEAD HEAD | grep -E '^\.github/workflows/.*\.ya?ml$' || true)
60 | if [ -n "$CHANGED" ]; then
61 | echo "::error::External contributors may not modify workflow files: $CHANGED"
62 | exit 1
63 | fi
64 |
--------------------------------------------------------------------------------
/examples/ex_10_readme_custom_registration.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | README Custom Type Registration Example
4 |
5 | This example demonstrates how to register custom encoders and decoders
6 | for types like Decimal and Path using the registration system.
7 | """
8 |
9 | from decimal import Decimal
10 | from pathlib import Path
11 | from typing import Any, Dict
12 |
13 | import kajson
14 | from kajson import kajson_manager
15 |
16 |
17 | def main():
18 | print("=== README Custom Type Registration Example ===\n")
19 |
20 | # Register Decimal support
21 | def encode_decimal(value: Decimal) -> Dict[str, str]:
22 | return {"decimal": str(value)}
23 |
24 | def decode_decimal(data: Dict[str, str]) -> Decimal:
25 | return Decimal(data["decimal"])
26 |
27 | kajson.UniversalJSONEncoder.register(Decimal, encode_decimal)
28 | kajson.UniversalJSONDecoder.register(Decimal, decode_decimal)
29 |
30 | print("✅ Registered Decimal encoder/decoder")
31 |
32 | # Now Decimal works seamlessly
33 | data = {"price": Decimal("19.99"), "tax": Decimal("1.50")}
34 | print(f"Original data: {data}")
35 | print(f"Price type: {type(data['price'])}")
36 |
37 | json_str = kajson.dumps(data)
38 | print(f"\nSerialized JSON: {json_str}")
39 |
40 | restored = kajson.loads(json_str)
41 | print(f"\nRestored data: {restored}")
42 | print(f"Restored price type: {type(restored['price'])}")
43 |
44 | assert restored["price"] == Decimal("19.99") # ✅
45 | print("✅ Decimal values match perfectly!")
46 |
47 | # Register Path support - handle both abstract and concrete types
48 | def encode_path(p: Path) -> Dict[str, Any]:
49 | return {"path": str(p)}
50 |
51 | def decode_path(data: Dict[str, Any]) -> Path:
52 | return Path(data["path"])
53 |
54 | kajson.UniversalJSONEncoder.register(Path, encode_path)
55 | kajson.UniversalJSONDecoder.register(Path, decode_path)
56 |
57 | # Also register for the concrete Path type (PosixPath/WindowsPath)
58 | concrete_path_type = type(Path())
59 | if concrete_path_type != Path:
60 | kajson.UniversalJSONEncoder.register(concrete_path_type, encode_path)
61 | kajson.UniversalJSONDecoder.register(concrete_path_type, decode_path)
62 |
63 | print("\n✅ Registered Path encoder/decoder")
64 |
65 | # Path objects now work too!
66 | config = {"home": Path.home(), "config": Path("/etc/myapp/config.json")}
67 | print(f"Config with paths: {config}")
68 |
69 | restored_config = kajson.loads(kajson.dumps(config))
70 | print(f"Restored config: {restored_config}")
71 | print(f"Home path type: {type(restored_config['home'])}")
72 | print("✅ Path objects work seamlessly!")
73 |
74 |
75 | if __name__ == "__main__":
76 | kajson_manager.KajsonManager()
77 | main()
78 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: "🐞 Bug report"
2 | description: "Report a reproducible problem with the library"
3 | type: Bug
4 | labels:
5 | - status:needs-triage
6 |
7 | body:
8 | - type: markdown
9 | attributes:
10 | value: |
11 | **Thanks for taking the time to file a bug!**
12 | Please complete **all required sections**—incomplete reports will be sent back for more information.
13 |
14 | - type: checkboxes
15 | id: confirmations
16 | attributes:
17 | label: "Before submitting"
18 | options:
19 | - label: "I'm using the **latest released** version of the library"
20 | required: true
21 | - label: "I've searched [open issues](issues?q=is%3Aissue%20state%3Aopen%20type%3ABug) and found no duplicate"
22 | required: true
23 |
24 | - type: textarea
25 | id: description
26 | attributes:
27 | label: "Describe the bug, tell us what went wrong"
28 | placeholder: "A clear and concise description of what went wrong. What did you expect to happen vs. what actually happened."
29 | validations:
30 | required: true
31 |
32 | - type: textarea
33 | id: reproduction
34 | attributes:
35 | label: "Reproduction snippet"
36 | description: "How can we reproduce the bug?"
37 | placeholder: |
38 | Provide the cli command that reproduces the bug:
39 | ```bash
40 | # paste command here
41 | ```
42 | or the smallest possible script that reproduces the bug:
43 | ```python
44 | # paste code here
45 | ```
46 | validations:
47 | required: false
48 |
49 | - type: input
50 | id: lib_version
51 | attributes:
52 | label: "Library version"
53 | placeholder: "e.g. 1.4.2"
54 | validations:
55 | required: true
56 |
57 | - type: input
58 | id: python_version
59 | attributes:
60 | label: "Python version"
61 | placeholder: "e.g. 3.12.0"
62 | validations:
63 | required: true
64 |
65 | - type: input
66 | id: os
67 | attributes:
68 | label: "Operating system"
69 | placeholder: "e.g. Ubuntu 22.04 LTS / MacOS 14.3"
70 | validations:
71 | required: false
72 |
73 | - type: textarea
74 | id: logs
75 | attributes:
76 | label: "Stack trace / error output"
77 | description: "Paste any relevant logs here."
78 | render: shell
79 | validations:
80 | required: false
81 |
82 | - type: textarea
83 | id: extra
84 | attributes:
85 | label: "Additional context & screenshots"
86 | placeholder: "Anything else that might help us debug."
87 | validations:
88 | required: false
89 |
90 | - type: dropdown
91 | id: contribution
92 | attributes:
93 | label: Would you like to help fix this issue?
94 | description: We welcome contributions and can provide guidance for first-time contributors!
95 | options:
96 | - "Not at this time"
97 | - "Yes, I'd like to contribute"
98 | validations:
99 | required: true
100 |
--------------------------------------------------------------------------------
/tests/unit/kajson_api/test_time_encoder_decoder.py:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: © 2025 Evotis S.A.S.
2 | # SPDX-License-Identifier: Apache-2.0
3 |
4 | import datetime
5 | from zoneinfo import ZoneInfo
6 |
7 | from kajson import kajson
8 |
9 |
10 | class TestTimeEncoderDecoder:
11 | """Test cases for time encoding and decoding functions."""
12 |
13 | def test_json_encode_time_naive(self) -> None:
14 | """Test json_encode_time with naive time (covers line 152)."""
15 | test_time = datetime.time(14, 30, 45, 123456)
16 | result = kajson.json_encode_time(test_time)
17 |
18 | expected = {"time": "14:30:45.123456", "tzinfo": None}
19 | assert result == expected
20 |
21 | def test_json_encode_time_with_timezone(self) -> None:
22 | """Test json_encode_time with timezone-aware time."""
23 | timezone = ZoneInfo("America/New_York")
24 | test_time = datetime.time(14, 30, 45, 123456, tzinfo=timezone)
25 | result = kajson.json_encode_time(test_time)
26 |
27 | expected = {"time": "14:30:45.123456", "tzinfo": timezone}
28 | assert result == expected
29 |
30 | def test_json_decode_time_naive(self) -> None:
31 | """Test json_decode_time with naive time (covers lines 163, 172-180)."""
32 | test_dict = {"time": "14:30:45.123456", "tzinfo": None}
33 | result = kajson.json_decode_time(test_dict)
34 |
35 | expected = datetime.time(14, 30, 45, 123456)
36 | assert result == expected
37 | assert result.tzinfo is None
38 |
39 | def test_json_decode_time_with_timezone(self) -> None:
40 | """Test json_decode_time with timezone."""
41 | timezone = ZoneInfo("UTC")
42 | test_dict = {"time": "14:30:45.123456", "tzinfo": timezone}
43 | result = kajson.json_decode_time(test_dict)
44 |
45 | expected = datetime.time(14, 30, 45, 123456, tzinfo=timezone)
46 | assert result == expected
47 | assert result.tzinfo is timezone
48 |
49 | def test_json_decode_time_zero_microseconds(self) -> None:
50 | """Test json_decode_time with zero microseconds."""
51 | test_dict = {"time": "14:30:45.000000", "tzinfo": None}
52 | result = kajson.json_decode_time(test_dict)
53 |
54 | expected = datetime.time(14, 30, 45, 0)
55 | assert result == expected
56 |
57 | def test_json_decode_time_no_microseconds(self) -> None:
58 | """Test json_decode_time parsing different time formats."""
59 | # Test with small microseconds value
60 | test_dict = {"time": "09:15:30.000001", "tzinfo": None}
61 | result = kajson.json_decode_time(test_dict)
62 |
63 | expected = datetime.time(9, 15, 30, 1)
64 | assert result == expected
65 |
66 | def test_time_roundtrip_serialization(self) -> None:
67 | """Test complete time serialization roundtrip."""
68 | original_time = datetime.time(10, 30, 45, 123456)
69 |
70 | # Encode
71 | json_str = kajson.dumps(original_time)
72 |
73 | # Decode
74 | decoded_time = kajson.loads(json_str)
75 |
76 | assert decoded_time == original_time
77 | assert isinstance(decoded_time, datetime.time)
78 |
--------------------------------------------------------------------------------
/tests/unit/kajson_api/test_datetime_encoder_decoder.py:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: © 2025 Evotis S.A.S.
2 | # SPDX-License-Identifier: Apache-2.0
3 |
4 | import datetime
5 | from zoneinfo import ZoneInfo
6 |
7 | import pytest
8 |
9 | from kajson import kajson
10 | from kajson.exceptions import KajsonDecoderError
11 |
12 |
13 | class TestDatetimeEncoderDecoder:
14 | """Test cases for datetime encoding and decoding functions."""
15 |
16 | def test_json_encode_datetime_naive(self) -> None:
17 | """Test json_encode_datetime with naive datetime (covers lines 126-127)."""
18 | test_datetime = datetime.datetime(2023, 12, 25, 14, 30, 45, 123456)
19 | result = kajson.json_encode_datetime(test_datetime)
20 |
21 | expected = {"datetime": "2023-12-25 14:30:45.123456", "tzinfo": None}
22 | assert result == expected
23 |
24 | def test_json_encode_datetime_with_timezone(self) -> None:
25 | """Test json_encode_datetime with timezone-aware datetime."""
26 | timezone = ZoneInfo("America/New_York")
27 | test_datetime = datetime.datetime(2023, 12, 25, 14, 30, 45, 123456, tzinfo=timezone)
28 | result = kajson.json_encode_datetime(test_datetime)
29 |
30 | expected = {"datetime": "2023-12-25 14:30:45.123456", "tzinfo": "America/New_York"}
31 | assert result == expected
32 |
33 | def test_json_decode_datetime_naive(self) -> None:
34 | """Test json_decode_datetime with naive datetime."""
35 | test_dict = {"datetime": "2023-12-25 14:30:45.123456", "tzinfo": None}
36 | result = kajson.json_decode_datetime(test_dict)
37 |
38 | expected = datetime.datetime(2023, 12, 25, 14, 30, 45, 123456)
39 | assert result == expected
40 | assert result.tzinfo is None
41 |
42 | def test_json_decode_datetime_with_timezone(self) -> None:
43 | """Test json_decode_datetime with timezone."""
44 | test_dict = {"datetime": "2023-12-25 14:30:45.123456", "tzinfo": "UTC"}
45 | result = kajson.json_decode_datetime(test_dict)
46 |
47 | expected = datetime.datetime(2023, 12, 25, 14, 30, 45, 123456, tzinfo=ZoneInfo("UTC"))
48 | assert result == expected
49 | assert result.tzinfo is not None
50 |
51 | def test_json_decode_datetime_missing_datetime_field(self) -> None:
52 | """Test json_decode_datetime with missing datetime field (covers line 149)."""
53 | test_dict = {"tzinfo": "UTC"}
54 |
55 | with pytest.raises(KajsonDecoderError) as excinfo:
56 | kajson.json_decode_datetime(test_dict)
57 |
58 | assert "Could not decode datetime from json: datetime field is required" in str(excinfo.value)
59 |
60 | def test_datetime_roundtrip_serialization(self) -> None:
61 | """Test complete datetime serialization roundtrip."""
62 | original_datetime = datetime.datetime(2023, 6, 15, 10, 30, 45, 123456, tzinfo=ZoneInfo("Europe/Paris"))
63 |
64 | # Encode
65 | json_str = kajson.dumps(original_datetime)
66 |
67 | # Decode
68 | decoded_datetime = kajson.loads(json_str)
69 |
70 | assert decoded_datetime == original_datetime
71 | assert isinstance(decoded_datetime, datetime.datetime)
72 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: Kajson Documentation
2 | site_url: https://pipelex.github.io/kajson/
3 | site_description: "Universal JSON encoder/decoder for Python with native support for Pydantic v2, datetime objects, and custom types."
4 | docs_dir: docs
5 | repo_url: "https://github.com/Pipelex/kajson"
6 | repo_name: "Kajson on GitHub"
7 | watch: [README.md, CONTRIBUTING.md, CHANGELOG.md, LICENSE]
8 |
9 | theme:
10 | name: material
11 | favicon: images/favicon.png
12 | features:
13 | - navigation.footer
14 | - navigation.tabs
15 | - navigation.tabs.sticky
16 | - navigation.sections
17 | - navigation.top
18 | - content.code.copy
19 | - content.code.annotate
20 | palette:
21 | - scheme: default # light
22 | primary: teal
23 | accent: deep purple
24 | toggle:
25 | icon: material/weather-night
26 | name: Switch to dark mode
27 | - scheme: slate # dark
28 | primary: teal
29 | accent: purple
30 | toggle:
31 | icon: material/weather-sunny
32 | name: Switch to light mode
33 |
34 | copyright: "© 2025 Evotis S.A.S.
Licensed under Apache 2.0"
35 |
36 | extra:
37 | social:
38 | - icon: fontawesome/brands/github
39 | link: https://github.com/Pipelex/kajson
40 | name: Kajson on GitHub
41 | - icon: fontawesome/brands/python
42 | link: https://pypi.org/project/kajson/
43 | name: kajson on PyPI
44 | - icon: fontawesome/brands/discord
45 | link: https://go.pipelex.com/discord
46 | name: Pipelex on Discord
47 | generator: false
48 |
49 | plugins:
50 | - search
51 | - meta-manager
52 |
53 | markdown_extensions:
54 | - meta
55 | - attr_list
56 | - md_in_html
57 | - admonition
58 | - pymdownx.details
59 | - pymdownx.superfences
60 | - pymdownx.highlight:
61 | anchor_linenums: true
62 | - pymdownx.inlinehilite
63 | - pymdownx.snippets:
64 | base_path: .
65 | check_paths: true
66 | - pymdownx.tabbed:
67 | alternate_style: true
68 | - pymdownx.emoji:
69 | emoji_index: !!python/name:material.extensions.emoji.twemoji
70 | emoji_generator: !!python/name:material.extensions.emoji.to_svg
71 |
72 | nav:
73 | - Home:
74 | - Welcome: index.md
75 | - Installation: pages/installation.md
76 | - Quick Start: pages/quick-start.md
77 | - Examples: pages/examples/index.md
78 | - User Guide:
79 | - Overview: pages/guide/overview.md
80 | - Basic Usage: pages/guide/basic-usage.md
81 | - Pydantic Integration: pages/guide/pydantic.md
82 | - Custom Types: pages/guide/custom-types.md
83 | - Class Registry: pages/guide/class-registry.md
84 | - Error Handling: pages/guide/error-handling.md
85 | - API Reference:
86 | - kajson module: pages/api/kajson.md
87 | - Encoder: pages/api/encoder.md
88 | - Decoder: pages/api/decoder.md
89 | - Manager: pages/api/manager.md
90 | - Contributing:
91 | - Guidelines: contributing.md
92 | - Code of Conduct: CODE_OF_CONDUCT.md
93 | - About:
94 | - License: license.md
95 | - Changelog: changelog.md
96 | - Credits: pages/credits.md
97 |
98 | extra_css:
99 | - stylesheets/extra.css
100 |
--------------------------------------------------------------------------------
/tests/unit/kajson_api/test_encoder_registration.py:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: © 2025 Evotis S.A.S.
2 | # SPDX-License-Identifier: Apache-2.0
3 |
4 | import datetime
5 |
6 | from kajson import kajson
7 |
8 |
9 | class TestEncoderRegistration:
10 | """Test that encoders are properly registered."""
11 |
12 | def test_date_encoder_registered(self) -> None:
13 | """Test that date encoder is registered with UniversalJSONEncoder."""
14 | from kajson.json_encoder import UniversalJSONEncoder
15 |
16 | assert UniversalJSONEncoder.is_encoder_registered(datetime.date)
17 | encoder_func = UniversalJSONEncoder.get_registered_encoder(datetime.date)
18 | assert encoder_func is kajson.json_encode_date
19 |
20 | def test_datetime_encoder_registered(self) -> None:
21 | """Test that datetime encoder is registered with UniversalJSONEncoder."""
22 | from kajson.json_encoder import UniversalJSONEncoder
23 |
24 | assert UniversalJSONEncoder.is_encoder_registered(datetime.datetime)
25 | encoder_func = UniversalJSONEncoder.get_registered_encoder(datetime.datetime)
26 | assert encoder_func is kajson.json_encode_datetime
27 |
28 | def test_time_encoder_registered(self) -> None:
29 | """Test that time encoder is registered with UniversalJSONEncoder."""
30 | from kajson.json_encoder import UniversalJSONEncoder
31 |
32 | assert UniversalJSONEncoder.is_encoder_registered(datetime.time)
33 | encoder_func = UniversalJSONEncoder.get_registered_encoder(datetime.time)
34 | assert encoder_func is kajson.json_encode_time
35 |
36 | def test_timedelta_encoder_registered(self) -> None:
37 | """Test that timedelta encoder is registered with UniversalJSONEncoder."""
38 | from kajson.json_encoder import UniversalJSONEncoder
39 |
40 | assert UniversalJSONEncoder.is_encoder_registered(datetime.timedelta)
41 | encoder_func = UniversalJSONEncoder.get_registered_encoder(datetime.timedelta)
42 | assert encoder_func is kajson.json_encode_timedelta
43 |
44 | def test_date_decoder_registered(self) -> None:
45 | """Test that date decoder is registered with UniversalJSONDecoder."""
46 | from kajson.json_decoder import UniversalJSONDecoder
47 |
48 | assert UniversalJSONDecoder.is_decoder_registered(datetime.date)
49 | decoder_func = UniversalJSONDecoder.get_registered_decoder(datetime.date)
50 | assert decoder_func is kajson.json_decode_date
51 |
52 | def test_datetime_decoder_registered(self) -> None:
53 | """Test that datetime decoder is registered with UniversalJSONDecoder."""
54 | from kajson.json_decoder import UniversalJSONDecoder
55 |
56 | assert UniversalJSONDecoder.is_decoder_registered(datetime.datetime)
57 | decoder_func = UniversalJSONDecoder.get_registered_decoder(datetime.datetime)
58 | assert decoder_func is kajson.json_decode_datetime
59 |
60 | def test_time_decoder_registered(self) -> None:
61 | """Test that time decoder is registered with UniversalJSONDecoder."""
62 | from kajson.json_decoder import UniversalJSONDecoder
63 |
64 | assert UniversalJSONDecoder.is_decoder_registered(datetime.time)
65 | decoder_func = UniversalJSONDecoder.get_registered_decoder(datetime.time)
66 | assert decoder_func is kajson.json_decode_time
67 |
--------------------------------------------------------------------------------
/docs/pages/api/manager.md:
--------------------------------------------------------------------------------
1 | # KajsonManager API Reference
2 |
3 | The `KajsonManager` class provides a singleton interface for managing kajson operations, including class registry management and logger configuration.
4 |
5 | ## KajsonManager Class
6 |
7 | ### Constructor
8 |
9 | ```python
10 | def __init__(
11 | self,
12 | logger_channel_name: Optional[str] = None,
13 | class_registry: Optional[ClassRegistryAbstract] = None,
14 | ) -> None
15 | ```
16 |
17 | Initialize the KajsonManager singleton instance.
18 |
19 | **Parameters:**
20 |
21 | - `logger_channel_name`: Name of the logger channel (default: "kajson")
22 | - `class_registry`: Custom class registry implementation (default: ClassRegistry())
23 |
24 | !!! note
25 | KajsonManager is a singleton class. Multiple calls to the constructor will return the same instance.
26 |
27 | ### Class Methods
28 |
29 | #### get_instance
30 |
31 | ```python
32 | @classmethod
33 | def get_instance(cls) -> KajsonManager
34 | ```
35 |
36 | Get the singleton instance of KajsonManager. Creates one if it doesn't exist.
37 |
38 | **Returns:** The singleton KajsonManager instance
39 |
40 | **Example:**
41 |
42 | ```python
43 | from kajson.kajson_manager import KajsonManager
44 |
45 | manager = KajsonManager.get_instance()
46 | ```
47 |
48 | #### teardown
49 |
50 | ```python
51 | @classmethod
52 | def teardown(cls) -> None
53 | ```
54 |
55 | Destroy the singleton instance. Useful for testing or cleanup scenarios.
56 |
57 | **Example:**
58 |
59 | ```python
60 | from kajson.kajson_manager import KajsonManager
61 |
62 | # Clean up the singleton instance
63 | KajsonManager.teardown()
64 | ```
65 |
66 | #### get_class_registry
67 |
68 | ```python
69 | @classmethod
70 | def get_class_registry(cls) -> ClassRegistryAbstract
71 | ```
72 |
73 | Get the class registry from the singleton instance.
74 |
75 | **Returns:** The class registry instance used for managing custom type serialization
76 |
77 | **Example:**
78 |
79 | ```python
80 | from kajson.kajson_manager import KajsonManager
81 |
82 | registry = KajsonManager.get_class_registry()
83 | ```
84 |
85 | ## Usage Examples
86 |
87 | ### Basic Usage
88 |
89 | ```python
90 | from kajson.kajson_manager import KajsonManager
91 |
92 | # Get the singleton instance
93 | manager = KajsonManager.get_instance()
94 |
95 | # Access the class registry
96 | registry = manager._class_registry
97 | # or use the class method
98 | registry = KajsonManager.get_class_registry()
99 | ```
100 |
101 | ### Custom Configuration
102 |
103 | ```python
104 | from kajson.kajson_manager import KajsonManager
105 | from kajson.class_registry import ClassRegistry
106 |
107 | # Initialize with custom logger channel
108 | manager = KajsonManager(logger_channel_name="my_logger")
109 |
110 | # Or with custom class registry
111 | custom_registry = ClassRegistry()
112 | manager = KajsonManager(class_registry=custom_registry)
113 | ```
114 |
115 | ### Testing and Cleanup
116 |
117 | ```python
118 | from kajson.kajson_manager import KajsonManager
119 |
120 | # In test setup - ensure clean state
121 | KajsonManager.teardown()
122 |
123 | # Use the manager in tests
124 | manager = KajsonManager.get_instance()
125 |
126 | # In test teardown
127 | KajsonManager.teardown()
128 | ```
129 |
130 | !!! tip
131 | The KajsonManager is primarily used internally by kajson. Most users won't need to interact with it directly unless they're implementing custom serialization logic or need to access the class registry programmatically.
--------------------------------------------------------------------------------
/docs/pages/quick-start.md:
--------------------------------------------------------------------------------
1 | # Quick Start Guide
2 |
3 | Get up and running with Kajson in minutes! This guide covers the most common use cases.
4 |
5 | ## Basic Usage
6 |
7 | Kajson is designed as a drop-in replacement for Python's standard `json` module:
8 |
9 | ```python
10 | import kajson
11 |
12 | # Works just like standard json
13 | data = {"name": "Alice", "age": 30}
14 | json_str = kajson.dumps(data)
15 | restored = kajson.loads(json_str)
16 | ```
17 |
18 | ## Working with Pydantic Models
19 |
20 | The real power of Kajson comes when working with complex objects:
21 |
22 | ```python
23 | from datetime import datetime
24 | from pydantic import BaseModel
25 | import kajson
26 |
27 | class User(BaseModel):
28 | name: str
29 | email: str
30 | created_at: datetime
31 | is_active: bool = True
32 |
33 | # Create a user
34 | user = User(
35 | name="Alice",
36 | email="alice@example.com",
37 | created_at=datetime.now()
38 | )
39 |
40 | # Serialize to JSON
41 | json_str = kajson.dumps(user, indent=2)
42 | print(json_str)
43 |
44 | # Deserialize back to User object
45 | restored_user = kajson.loads(json_str)
46 | assert isinstance(restored_user, User)
47 | assert user == restored_user
48 | ```
49 |
50 | ## DateTime Support
51 |
52 | Kajson automatically handles datetime objects:
53 |
54 | ```python
55 | from datetime import datetime, date, time, timedelta
56 | import kajson
57 |
58 | data = {
59 | "meeting_date": date(2025, 1, 15),
60 | "meeting_time": time(14, 30),
61 | "meeting_datetime": datetime(2025, 1, 15, 14, 30),
62 | "duration": timedelta(hours=1, minutes=30)
63 | }
64 |
65 | # Serialize and deserialize
66 | json_str = kajson.dumps(data)
67 | restored = kajson.loads(json_str)
68 |
69 | # All types are preserved
70 | assert isinstance(restored["meeting_date"], date)
71 | assert isinstance(restored["duration"], timedelta)
72 | ```
73 |
74 | ## Nested Objects
75 |
76 | Kajson handles complex nested structures seamlessly:
77 |
78 | ```python
79 | from typing import List
80 | from pydantic import BaseModel
81 | from datetime import datetime
82 |
83 | class Comment(BaseModel):
84 | text: str
85 | author: str
86 | created_at: datetime
87 |
88 | class Post(BaseModel):
89 | title: str
90 | content: str
91 | comments: List[Comment]
92 | tags: List[str]
93 |
94 | # Create nested structure
95 | post = Post(
96 | title="Getting Started with Kajson",
97 | content="Kajson makes JSON serialization easy...",
98 | comments=[
99 | Comment(text="Great post!", author="Bob", created_at=datetime.now()),
100 | Comment(text="Very helpful", author="Carol", created_at=datetime.now())
101 | ],
102 | tags=["python", "json", "tutorial"]
103 | )
104 |
105 | # Works perfectly
106 | json_str = kajson.dumps(post)
107 | restored_post = kajson.loads(json_str)
108 | ```
109 |
110 | ## File Operations
111 |
112 | Just like standard json, Kajson supports file operations:
113 |
114 | ```python
115 | import kajson
116 |
117 | # Save to file
118 | with open("data.json", "w") as f:
119 | kajson.dump(user, f, indent=2)
120 |
121 | # Load from file
122 | with open("data.json", "r") as f:
123 | loaded_user = kajson.load(f)
124 | ```
125 |
126 | ## Next Steps
127 |
128 | - Learn about [Basic Usage](guide/basic-usage.md) patterns
129 | - Explore [Pydantic Integration](guide/pydantic.md) in depth
130 | - Discover how to work with [Custom Types](guide/custom-types.md)
131 | - Check out practical [Examples](examples/index.md)
132 |
--------------------------------------------------------------------------------
/docs/pages/guide/overview.md:
--------------------------------------------------------------------------------
1 | # User Guide Overview
2 |
3 | Welcome to the Kajson User Guide! This comprehensive guide will help you master all aspects of Kajson, from basic usage to advanced features.
4 |
5 | ## What You'll Learn
6 |
7 | This guide is organized into several sections, each focusing on different aspects of Kajson:
8 |
9 | ### [Basic Usage](basic-usage.md)
10 | Learn the fundamentals of using Kajson as a drop-in replacement for Python's standard json module. Covers:
11 |
12 | - Basic serialization and deserialization
13 | - Working with files
14 | - Formatting options
15 | - Common patterns
16 |
17 | ### [Pydantic Integration](pydantic.md)
18 | Discover how Kajson seamlessly integrates with Pydantic v2 models:
19 |
20 | - Automatic model serialization
21 | - Validation during deserialization
22 | - Nested model support
23 | - Advanced Pydantic features
24 |
25 | ### [Custom Types](custom-types.md)
26 | Learn how to extend Kajson to support your own custom types:
27 |
28 | - Registering custom encoders/decoders
29 | - Using the `__json_encode__` and `__json_decode__` hooks
30 | - Best practices for custom type support
31 | - Common patterns and examples
32 |
33 | ### [Error Handling](error-handling.md)
34 | Understand how to handle errors and edge cases:
35 |
36 | - Common error scenarios
37 | - Validation errors with Pydantic
38 | - Debugging serialization issues
39 | - Best practices for robust code
40 |
41 | ## Key Concepts
42 |
43 | ### Type Preservation
44 |
45 | Kajson's main innovation is automatic type preservation. When you serialize an object, Kajson adds metadata that allows perfect reconstruction:
46 |
47 | ```python
48 | from datetime import datetime
49 | import kajson
50 |
51 | # Original object
52 | data = {"created": datetime.now(), "count": 42}
53 |
54 | # Serialize
55 | json_str = kajson.dumps(data)
56 |
57 | # Deserialize - types are preserved!
58 | restored = kajson.loads(json_str)
59 | assert isinstance(restored["created"], datetime)
60 | ```
61 |
62 | ### Drop-in Replacement
63 |
64 | Kajson is designed to be a drop-in replacement for the standard `json` module:
65 |
66 | ```python
67 | # Change this:
68 | import json
69 |
70 | # To this:
71 | import kajson as json
72 |
73 | # All your existing code continues to work!
74 | ```
75 |
76 | ### Extensibility
77 |
78 | Kajson is built to be extensible. You can easily add support for any type:
79 |
80 | ```python
81 | import kajson
82 | from decimal import Decimal
83 |
84 | # Register Decimal support
85 | kajson.UniversalJSONEncoder.register(
86 | Decimal,
87 | lambda d: {"value": str(d)}
88 | )
89 | kajson.UniversalJSONDecoder.register(
90 | Decimal,
91 | lambda data: Decimal(data["value"])
92 | )
93 | ```
94 |
95 | ## Best Practices
96 |
97 | 1. **Use Type Hints**: Always use type hints in your Pydantic models for better IDE support and documentation
98 | 2. **Handle Errors**: Always wrap deserialization in try-except blocks when dealing with untrusted data
99 | 3. **Test Serialization**: Test that your objects can round-trip (serialize and deserialize) correctly
100 | 4. **Keep It Simple**: Start with built-in support before creating custom encoders
101 |
102 | ## Getting Help
103 |
104 | - Check the [API Reference](../api/kajson.md) for detailed function documentation
105 | - Browse [Examples](../examples/index.md) for real-world use cases
106 | - Visit our [GitHub repository](https://github.com/Pipelex/kajson) for issues and discussions
107 |
108 | ## Next Steps
109 |
110 | Ready to dive in? Start with [Basic Usage](basic-usage.md) to learn the fundamentals!
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Kajson - Universal JSON Encoder/Decoder for Python"
3 | ---
4 |
5 | # Welcome to Kajson Documentation
6 |
7 | **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**.
8 |
9 |