Skip to main content

Know your object is a attribute type checker

Project description

                       ▄▄          ▄▄                      
▀████▀ ▀███▀          ▄██          ██                 ██   
  ██   ▄█▀             ██                             ██   
  ██ ▄█▀      ▄██▀██▄  ██▄████▄  ▀███  ▄▄█▀██ ▄██▀████████ 
  █████▄     ██▀   ▀██ ██    ▀██   ██ ▄█▀   ███▀  ██  ██   
  ██  ███    ██     ██ ██     ██   ██ ██▀▀▀▀▀▀█       ██   
  ██   ▀██▄  ██▄   ▄██ ██▄   ▄██   ██ ██▄    ▄█▄    ▄ ██   
▄████▄   ███▄ ▀█████▀  █▀█████▀    ██  ▀█████▀█████▀  ▀████
                                ██ ██                      
                                ▀███                       By CenturyBoys
                                
Know your object is a __init__ type validator for class and dataclass

CI codecov PyPI Python License

Installation

pip install kobject

Kobject has no required dependencies. For faster JSON encoding/decoding you can install the optional orjson extra — when present it is used automatically by to_json()/from_json(); otherwise the standard library json is used.

pip install "kobject[orjson]"

Usage

Kobject can be use inside default class declaration and with dataclasses. Kobject uses the __init__ signature to check types.

Default classes

from kobject import Kobject

class StubClass(Kobject):
    a_int: int
    a_bool: bool
    
    def __init__(
        self,
        a_int: int,
        a_bool: bool
    ):
        self.a_int = a_int
        self.a_bool = a_bool
        self.__post_init__()

instance = StubClass(a_int=1, a_bool=True)

Notice that in the default class declaration you need to call self.__post_init__() at the end of the __init__ declaration.

Dataclass

from dataclasses import dataclass
from kobject import Kobject

@dataclass
class StubClass(Kobject):
    a_int: int
    a_bool: bool

instance = StubClass(a_int=1, a_bool=True)

By default, dataclass calls self.__post_init__() at the end of the __init__ declaration doc.

Exception

Kobject raises TypeError with all validation errors, that means it checks all your object's attributes before raising the TypeError. Types like List, Tuple, and Set will have all their elements checked.

from dataclasses import dataclass
from kobject import Kobject
from typing import List, Tuple

@dataclass
class StubClass(Kobject):
    a_list_int: List[int]
    a_tuple_bool: Tuple[bool]

instance = StubClass(a_list_int=[1, "", 2, ""], a_tuple_bool=["", True])
Traceback (most recent call last):
...
TypeError: Class 'StubClass' type error:
 Wrong type for a_list_int: typing.List[int] != `[1, '', 2, '']`
 Wrong type for a_tuple_bool: typing.Tuple[bool] != `['', True]`

You can retrieve the structured error by calling json_error method

try:
    instance = StubClass(a_list_int=[1, "", 2, ""], a_tuple_bool=["", True])
except TypeError as _error:
    print(_error.json_error())

Output:

[{'field': 'a_list_int', 'type': typing.List[int], 'value': "[1, '', 2, '']"}, {'field': 'a_tuple_bool', 'type': typing.Tuple[bool], 'value': "['', True]"}]

You can use lazy validation to improve performance, the code will stop in the first found error for this use

from kobject import Kobject

Kobject.set_lazy_type_check(status=True)

Default value

Kobject supports default values and will check them before any validation, that means if you declare a a_bool: bool = None it will not raise an error.

from dataclasses import dataclass
from kobject import Kobject

class StubClass(Kobject):
    a_bool: bool = None

    def __init__(self, a_bool: bool = 10):
        self.a_bool = a_bool
        self.__post_init__()

@dataclass
class StubDataClass(Kobject):
    a_bool: bool = 10

Custom exception

By default, Kobject raise a TypeError but you can override this exception using set_validation_custom_exception for type validation or set_content_check_custom_exception for field check on from JSON operation.

from dataclasses import dataclass
from kobject import Kobject


class CustomException(Exception):
    pass


Kobject.set_validation_custom_exception(CustomException)
#Kobject.set_content_check_custom_exception(CustomException)

@dataclass
class StubClass(Kobject):
    a__int: int


instance = StubClass(a__int="")
Traceback (most recent call last):
...
CustomException: Class 'StubClass' type error:
 Wrong type for a__int: <class 'int'> != `''`

ToJSON

Kobject has his own implementation to parse class instance to a JSON representation.

from dataclasses import dataclass
from typing import List, Tuple

from kobject import Kobject
    
@dataclass
class BaseC(Kobject):
    a_int: int
    a_str: str
    a_list_of_int: List[int]
    a_tuple_of_bool: Tuple[bool]
    
instance = BaseC(
    a_int=1,
    a_str="lala",
    a_list_of_int=[1, 2, 3],
    a_tuple_of_bool=(True,)
)

json_bytes = instance.to_json()

print(json_bytes)
b'{"a_int": 1, "a_str": "lala", "a_list_of_int": [1, 2, 3], "a_tuple_of_bool": [true]}'

For complex values ToJSON expose set_encoder_resolver to handler it.

Notest, Before encoding the object to JSON bytes, it will be represented by self.dict(). Some objects, such as datetime.datetime, can be useful in dictionary structures but are not JSON serializable. In such cases, you can use the on_dict: bool parameter in the Kobject.set_encoder_resolver() method to encode only when JSON bytes are required, not in its dictionary representation.

from dataclasses import dataclass
from datetime import datetime
from typing import List
from uuid import UUID

from kobject import Kobject


@dataclass
class BaseA(Kobject):
    a_datetime: datetime


@dataclass
class BaseB:
    a_uuid: UUID


@dataclass
class BaseC(Kobject):
    a_base_a: BaseA
    a_base_b: BaseB
    a_list_of_base_a: List[BaseA]

Kobject.set_encoder_resolver(datetime, lambda value: str(value), False)
Kobject.set_encoder_resolver(BaseB, lambda value: {"a_uuid": str(value.a_uuid)})

instance = BaseC(
    a_base_a=BaseA(a_datetime=datetime.fromisoformat("2023-02-01 17:38:45.389426")),
    a_base_b=BaseB(a_uuid=UUID("1d9cf695-c917-49ce-854b-4063f0cda2e7")),
    a_list_of_base_a=[BaseA(a_datetime=datetime.fromisoformat("2023-02-01 17:38:45.389426"))]
)

dict_repr = instance.dict()

isinstance(dict_repr["a_base_a"]["a_datetime"], datetime)

json_bytes = instance.to_json()

print(json_bytes)
b'{"a_base_a": {"a_datetime": "2023-02-01 17:38:45.389426"}, "a_base_b": {"a_uuid": "1d9cf695-c917-49ce-854b-4063f0cda2e7"}, "a_list_of_base_a": [{"a_datetime": "2023-02-01 17:38:45.389426"}]}'

Remove None values

Both dict() and to_json() methods support the remove_nones parameter to recursively strip None values from the output.

from dataclasses import dataclass
from typing import List, Dict

from kobject import Kobject


@dataclass
class Inner(Kobject):
    value: str | None


@dataclass
class Outer(Kobject):
    a_int: int
    a_str: str | None
    a_list: List[int | None]
    a_dict: Dict[str, int | None]
    inner: Inner


instance = Outer(
    a_int=1,
    a_str=None,
    a_list=[1, None, 2],
    a_dict={"a": 1, "b": None},
    inner=Inner(value=None)
)

# Default behavior preserves None values
print(instance.dict())
# {'a_int': 1, 'a_str': None, 'a_list': [1, None, 2], 'a_dict': {'a': 1, 'b': None}, 'inner': {'value': None}}

# With remove_nones=True, None values are recursively removed
print(instance.dict(remove_nones=True))
# {'a_int': 1, 'a_list': [1, 2], 'a_dict': {'a': 1}, 'inner': {}}

# Also works with to_json()
print(instance.to_json(remove_nones=True))
# b'{"a_int":1,"a_list":[1,2],"a_dict":{"a":1},"inner":{}}'

FromJSON

Kobject has his own implementation to parse JSON to a class instance.

from dataclasses import dataclass
from typing import List, Tuple

from kobject import Kobject


@dataclass
class BaseC(Kobject):
    a_int: int
    a_str: str
    a_list_of_int: List[int]
    a_tuple_of_bool: Tuple[bool]

payload = (
    b'{"a_int": 1,"a_str": "lala","a_list_of_int": [1,2,3],'
    b'"a_tuple_of_bool": [true]}'
)
instance = BaseC.from_json(payload=payload)

print(instance)
BaseC(a_int=1, a_str='lala', a_list_of_int=[1, 2, 3], a_tuple_of_bool=(True,))

For complex values FromJSON expose set_decoder_resolver to handler it.

from datetime import datetime
from dataclasses import dataclass
from typing import List
from uuid import UUID

from kobject import Kobject


@dataclass
class BaseA(Kobject):
    a_datetime: datetime


@dataclass
class BaseB:
    a_uuid: UUID


@dataclass
class BaseC(Kobject):
    a_base_a: BaseA
    a_base_b: BaseB
    a_list_of_base_a: List[BaseA]

Kobject.set_decoder_resolver(
    datetime,
    lambda attr_type, value: datetime.fromisoformat(value)
    if isinstance(value, str)
    else value,
)
Kobject.set_decoder_resolver(
    BaseB,
    lambda attr_type, value: attr_type(a_uuid=UUID(value["a_uuid"]))
    if isinstance(value, dict)
    else value,
)
payload = (
    b'{"a_base_a": {"a_datetime": "2023-02-01 17:38:45.389426"},"a_base_b": {"a_'
    b'uuid":"1d9cf695-c917-49ce-854b-4063f0cda2e7"}, "a_lis'
    b't_of_base_a": [{"a_datetime": "2023-02-01 17:38:45.389426"}]}'
)
instance = BaseC.from_json(payload=payload)

print(instance)
BaseC(a_base_a=BaseA(a_datetime=datetime.datetime(2023, 2, 1, 17, 38, 45, 389426)), a_base_b=BaseB(a_uuid=UUID('1d9cf695-c917-49ce-854b-4063f0cda2e7')), a_list_of_base_a=[BaseA(a_datetime=datetime.datetime(2023, 2, 1, 17, 38, 45, 389426))])

Union types

When a field is a union (e.g. A | B, A | None, A | int), Kobject tries each member in the order it is declared and uses the first one that deserializes successfully. This also applies to unions used inside collections (list[A | B], set[A | B], tuple[A | B, ...], dict[str, A | B]).

There is no ambiguity detection: if a payload could match more than one member, the first declared member wins.

from dataclasses import dataclass

from kobject import Kobject


@dataclass
class A(Kobject):
    a: int


@dataclass
class B(Kobject):
    b: int


@dataclass
class C(Kobject):
    value: A | B


print(C.from_dict({"value": {"a": 1}}).value)  # A(a=1)
print(C.from_dict({"value": {"b": 2}}).value)  # B(b=2)
Tagged (discriminated) unions

When every Kobject member of a union shares a field annotated with a typing.Literal whose tag values are unique across the members, Kobject treats it as a tagged discriminated union. The tag in the payload selects the member directly, instead of trying members in declaration order. This is faster and produces a clear error against the selected member when the rest of the payload is malformed. No extra configuration is required — the discriminator is detected automatically from the Literal fields, and it also works inside collections (list[Cat | Dog], etc.).

from dataclasses import dataclass
from typing import Literal

from kobject import Kobject


@dataclass
class Cat(Kobject):
    kind: Literal["cat"]
    lives: int


@dataclass
class Dog(Kobject):
    kind: Literal["dog"]
    good: bool


@dataclass
class Owner(Kobject):
    pet: Cat | Dog


print(Owner.from_dict({"pet": {"kind": "dog", "good": True}}).pet)  # Dog(kind='dog', good=True)

If the tag value is unknown, or the members do not all share a unique Literal tag field, Kobject falls back to the first-match-wins behavior described above.

Literal types

typing.Literal fields are validated by membership: the value must be equal to one of the declared literals, matched by both value and type (so Literal[1] rejects True and Literal[True] rejects 1, per PEP 586). Literals are supported in validation, from_dict/from_json, and JSON Schema generation (emitted as an enum).

from dataclasses import dataclass
from typing import Literal

from kobject import Kobject


@dataclass
class Config(Kobject):
    mode: Literal["r", "w", "rw"]


Config(mode="rw")   # OK
Config(mode="x")    # Raises TypeError

Generic models (TypeVar)

A Kobject can be generic (class Box(Kobject, Generic[T])). When a parametrized generic is used as a field (Box[int]), Kobject binds the type variable to the concrete argument and validates, deserializes, and generates schema accordingly — including TypeVars nested inside collections (list[T], dict[str, T], ...).

from dataclasses import dataclass
from typing import Generic, TypeVar

from kobject import Kobject

T = TypeVar("T")


@dataclass
class Box(Kobject, Generic[T]):
    value: T


@dataclass
class Response(Kobject):
    data: Box[int]


Response.from_dict({"data": {"value": 5}})    # OK -> Response(data=Box(value=5))
Response(data=Box(value="x"))                 # Raises TypeError (value must be int)

Note: the binding is only enforced when the parametrized generic is used as a field (or through from_dict/from_json/json_schema of the enclosing model). A bare, unbound TypeVar — e.g. constructing Box(value=...) directly — is treated as Any, because Python does not make the Box[int] binding available during __init__.

JSON Schema

Kobject can generate JSON Schema (Draft 2020-12) from your class definition. This is useful for API documentation, validation, and integration with tools like MCP servers.

from dataclasses import dataclass
from kobject import Kobject
import json

@dataclass
class User(Kobject):
    """
    User model for the application.

    :param name: The user's full name.
    :param age: The user's age in years.
    :param email: Optional email address.
    :example: {"name": "Alice", "age": 30}
    """
    name: str
    age: int
    email: str | None = None

schema = User.json_schema()
print(json.dumps(schema, indent=2))
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "description": "The user's full name."
    },
    "age": {
      "type": "integer",
      "description": "The user's age in years."
    },
    "email": {
      "anyOf": [{"type": "string"}, {"type": "null"}],
      "description": "Optional email address.",
      "default": null
    }
  },
  "additionalProperties": false,
  "title": "User model for the application.",
  "required": ["name", "age"],
  "examples": [{"name": "Alice", "age": 30}]
}

Validation vs serialization schema

json_schema() accepts a mode:

  • mode="validation" (default) describes what from_dict()/from_json() accept: fields with defaults are optional (not listed in required).
  • mode="serialization" describes what to_dict()/to_json() emit: every field is always present, so all fields are required.
User.json_schema()                      # validation: required == ["name", "age"]
User.json_schema(mode="serialization")  # serialization: required == ["name", "age", "email"]

Docstring Metadata

Kobject extracts metadata from reST-style docstrings:

  • Title: First line of the docstring
  • Description: Text between title and first directive (if different from title)
  • Field descriptions: :param field_name: description
  • Examples: :example: {"json": "object"}

Supported Types

Python Type JSON Schema
str {"type": "string"}
int {"type": "integer"}
float {"type": "number"}
bool {"type": "boolean"}
None {"type": "null"}
list[T] {"type": "array", "items": {...}}
dict[K, V] {"type": "object", "additionalProperties": {...}}
tuple[X, Y] {"type": "array", "prefixItems": [...], "minItems": N, "maxItems": N}
set[T] {"type": "array", "items": {...}, "uniqueItems": true}
T | None {"anyOf": [{...}, {"type": "null"}]}
Enum {"type": "string/integer", "enum": [...]}
Kobject subclass {"$ref": "#/$defs/ClassName"}
datetime {"type": "string", "format": "date-time"}
date {"type": "string", "format": "date"}
time {"type": "string", "format": "time"}
UUID {"type": "string", "format": "uuid"}
Decimal {"type": "string", "pattern": "..."}

Nested Kobjects

Nested Kobject classes are handled using JSON Schema $ref and $defs:

from dataclasses import dataclass
from kobject import Kobject

@dataclass
class Address(Kobject):
    """
    Address information.

    :param street: Street name and number.
    :param city: City name.
    """
    street: str
    city: str

@dataclass
class Person(Kobject):
    """
    A person with an address.

    :param name: Person's name.
    :param address: Person's home address.
    """
    name: str
    address: Address

schema = Person.json_schema()
# schema["properties"]["address"] == {"$ref": "#/$defs/Address", "description": "..."}
# schema["$defs"]["Address"] contains the Address schema

Custom Schema Resolvers

For custom types, you can register schema resolvers using set_schema_resolver:

from dataclasses import dataclass
from kobject import Kobject

class Money:
    def __init__(self, amount: int, currency: str):
        self.amount = amount
        self.currency = currency

# Register a custom schema resolver
Kobject.set_schema_resolver(
    Money,
    lambda t: {
        "type": "object",
        "properties": {
            "amount": {"type": "integer"},
            "currency": {"type": "string", "minLength": 3, "maxLength": 3}
        },
        "required": ["amount", "currency"]
    }
)

@dataclass
class Invoice(Kobject):
    total: Money

schema = Invoice.json_schema()
# schema["properties"]["total"] contains the custom Money schema

Roadmap & design decisions

See docs/ROADMAP.md for what has recently shipped, what is deferred (e.g. field aliases, OpenAPI 3.1 output), and what is rejected by design (custom field validators and declarative value constraints) along with the rationale.

Project details


Download files

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

Source Distribution

kobject-0.8.0.tar.gz (40.0 kB view details)

Uploaded Source

Built Distribution

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

kobject-0.8.0-py3-none-any.whl (47.0 kB view details)

Uploaded Python 3

File details

Details for the file kobject-0.8.0.tar.gz.

File metadata

  • Download URL: kobject-0.8.0.tar.gz
  • Upload date:
  • Size: 40.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for kobject-0.8.0.tar.gz
Algorithm Hash digest
SHA256 2cb803aea62bfc64557304f7912af6ed50aa44035699b6bf17da653f20bd0668
MD5 8a0783ae5d5baed0b402c46a5dd9a853
BLAKE2b-256 e5335e450a9c68a2ffc08959a8c4330ad65b3dca7291d7e89a631ba968f0e575

See more details on using hashes here.

Provenance

The following attestation bundles were made for kobject-0.8.0.tar.gz:

Publisher: publish.yml on CenturyBoys/kobject

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

File details

Details for the file kobject-0.8.0-py3-none-any.whl.

File metadata

  • Download URL: kobject-0.8.0-py3-none-any.whl
  • Upload date:
  • Size: 47.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for kobject-0.8.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5c828891de030a6b1dfa74c6657546ab8c7ad2964810e38619f8ee6ace7cd9bb
MD5 582881186abef682366e25fdf799b20c
BLAKE2b-256 d37fdcf43ceecdd8a3b690ee0ce6e6f986d88c14995300833164f14a93c029a1

See more details on using hashes here.

Provenance

The following attestation bundles were made for kobject-0.8.0-py3-none-any.whl:

Publisher: publish.yml on CenturyBoys/kobject

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

Supported by

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