Kittie's attempt to declaratively transform Python objects into serializable dictionaries. Minimal, composable, Pydantic-free. Just fields, dataclasses, and a pinch of magic.
Features
- Declarative schema definition
- Field-level transformation
- Nested schemas
- Dependency-injected getters via FunDI
- No runtime overhead, no metaclasses, no opinionated data modeling
- Multiple getters(with fallback:
"created_at"
->"created"
) - Custom field metadata and schema metadata
from dataclasses import dataclass
from datetime import datetime
from kat_transform import schema, field, transform
user_schema = schema(
"User",
field(str, "username"),
field(int, "id")
)
@dataclass
class User:
username: str,
id: int
user = User("Kuyugama", -1)
raw = user_schema.get(user)
transformed = transform(raw)
assert transformed == {"username": "Kuyugama", "id": -1}
print(transformed)
from dataclasses import dataclass
from datetime import datetime
from kat_transform import schema, field, transform
user_schema = schema(
"User",
field(str, "username", transform=lambda x: x.lower()),
field(int, "created", transform=lambda x: int(x.timestamp()), getter=("created_at", "created")),
field(int, "id")
)
@dataclass
class User:
username: str,
created_at: datetime
id: int
user = User("Kuyugama", datetime(day=17, month=3, year=2026), -1)
raw = user_schema.get(user)
transformed = transform(raw)
assert transformed == {"username": "kuyugama", "created": 1773612000, "id": -1}
print(transformed)
from dataclasses import dataclass
from datetime import datetime
from kat_transform import schema, field, resolve_fields, transform
error_schema = schema(
"Error",
field(str, "message"),
field(str, "category"),
field(str, "code"),
field(str, "cat", transform=lambda x: f"https://http.cat/{x}", getter=lambda response: response["status_code"])
)
@dataclass
class Error:
message: str
category: str
code: str
error = Error("User not found", "users", "not-found")
raw = error_schema.get(error)
resolved = resolve_fields({"response": {"status_code": 404}}, raw)
transformed = transform(resolved)
assert transformed == {"message": "User not found", "category": "users", "code": "not-found", "cat": "https://http.cat/404"}
print(transformed)
resolve_fields
uses FunDI dependency injection under the hood. Allgetter
functions should be valid FunDI dependencies.
from dataclasses import dataclass
from datetime import datetime
from kat_transform import schema, field, resolve_fields, transform
user_schema = schema(
"User",
field(str, "username", transform=lambda x: x.lower()),
field(int, "id"),
)
content_schema = schema(
"Content",
field(user_schema, "owner"),
field(str, "title", transform=lambda x: x.title()),
)
@dataclass
class User:
username: str
id: int
@dataclass
class Content:
owner: User
title: str
user = User("Kuyugama", 1)
content = Content(user, "Clever flower")
raw = content_schema.get(content)
transformed = transform(raw)
assert transformed == {"owner": {"username": "kuyugama", "id": 1}, "title": "Clever Flower"}
print(transformed)