from etielle.core import MappingSpec, TraversalSpec, TableEmit, Field
from etielle.transforms import get
from etielle.executor import run_mapping
= {"users": [{"id": "u1", "email": "alice@example.com"}]}
data
= MappingSpec(traversals=[TraversalSpec(
spec =["users"],
path="auto",
mode=[TableEmit(
emits="users",
table=[get("id")],
join_keys=[Field("id", get("id")), Field("email", get("email"))]
fields
)]
)])= run_mapping(data, spec)
result # result["users"].instances = {("u1",): {"id": "u1", "email": "alice@example.com"}}
# ↑ plain dict
Instance emission (Pydantic, TypedDict, ORM)
What you’ll learn: How to emit Pydantic models or ORM objects directly instead of plain dicts, enabling validation and type safety.
Prerequisites: Basic understanding of TableEmit
and Field
from the README Quick Start.
What is instance emission?
Instead of getting back plain dicts from your mapping, you can tell etielle to create your Pydantic models, TypedDicts, or ORM objects directly.
This means you get:
- Validated data: Pydantic validates as it builds
- Type safety: Your IDE knows the exact type of each instance
- ORM integration: Create database objects without manual conversion
Progressive construction means you can have multiple traversals updating the same instance. For example:
- Traversal 1 sets
id
andname
fromusers
array - Traversal 2 adds
email
fromprofiles
array - Both updates merge into one
User
instance with matchingjoin_keys
TableEmit vs. InstanceEmit
With TableEmit (basic approach):
With InstanceEmit (typed approach):
from etielle.core import MappingSpec, TraversalSpec, field_of
from etielle.transforms import get
from etielle.instances import InstanceEmit, FieldSpec, PydanticBuilder
from etielle.executor import run_mapping
from pydantic import BaseModel
class User(BaseModel):
id: str
str
email:
= {"users": [{"id": "u1", "email": "alice@example.com"}]}
data
= MappingSpec(traversals=[TraversalSpec(
spec =["users"],
path="auto",
mode=[InstanceEmit[User](
emits="users",
table=[get("id")],
join_keys=PydanticBuilder(User),
builder=[
fields=field_of(User, lambda u: u.id), transform=get("id")),
FieldSpec(selector=field_of(User, lambda u: u.email), transform=get("email")),
FieldSpec(selector
]
)]
)])= run_mapping(data, spec)
result # result["users"].instances = {("u1",): User(id="u1", email="alice@example.com")}
# ↑ Pydantic model instance
Builders
from etielle.instances import InstanceEmit, FieldSpec, PydanticBuilder, TypedDictBuilder
from etielle.transforms import get
from etielle.core import MappingSpec, TraversalSpec, field_of
from pydantic import BaseModel
class User(BaseModel):
id: str
str
email:
= InstanceEmit[User](
emit ="users",
table=[get("id")],
join_keys=[
fields=field_of(User, lambda u: u.id), transform=get("id")),
FieldSpec(selector=field_of(User, lambda u: u.email), transform=get("email")),
FieldSpec(selector
],=PydanticBuilder(User),
builder
)
# Minimal runnable demo
= {"users": [{"id": "u1", "email": "alice@example.com"}]}
root = MappingSpec(traversals=[TraversalSpec(path=["users"], mode="auto", emits=[emit])])
mapping from etielle.executor import run_mapping
= run_mapping(root, mapping)
res print(sorted([(k, v.email) for k, v in res["users"].instances.items()]))
[(('u1',), 'alice@example.com')]
TypedDict without Pydantic:
from typing import TypedDict
class UserTD(TypedDict):
id: str
str
email:
= InstanceEmit[UserTD](
emit_td ="users",
table=[get("id")],
join_keys=[
fields="id", transform=get("id")),
FieldSpec(selector="email", transform=get("email")),
FieldSpec(selector
],=TypedDictBuilder(lambda d: UserTD(**d)),
builder
)
# Minimal runnable demo
= {"users": [{"id": "u1", "email": "alice@example.com"}]}
root = MappingSpec(traversals=[TraversalSpec(path=["users"], mode="auto", emits=[emit_td])])
mapping from etielle.executor import run_mapping
= run_mapping(root, mapping)
res print(list(res["users"].instances.values()))
[{'id': 'u1', 'email': 'alice@example.com'}]
Choosing a builder
PydanticBuilder(Model)
: Use for Pydantic models with validationPydanticPartialBuilder(Model)
: Use when some fields might be missing (creates partial models)TypedDictBuilder(factory)
: Use for plain dicts or when you don’t want Pydantic- Custom builder: Implement the builder protocol for ORM objects or custom types
Strictness and error collection
Builders collect update-time and finalize-time errors; the executor returns them in MappingResult
per table.
from etielle.executor import run_mapping
= run_mapping(root_err, mapping_err)
result = result["users"]
mr print(mr.update_errors)
print(mr.finalize_errors)
{('u1',): ["table=users key=('u1',) field emali: unknown field; did you mean email?"]}
{('u1',): ["table=users key=('u1',) 1 validation error for User\nemail\n Field required [type=missing, input_value={'id': 'u1'}, input_type=dict]\n For further information visit https://errors.pydantic.dev/2.12/v/missing"]}
Merge policies
By default, if two traversals update the same field, the last write wins. You can change this behavior—see Merge policies for details.
Reference
InstanceEmit[T]
FieldSpec[T]
PydanticBuilder
,PydanticPartialBuilder
,TypedDictBuilder
See also
- Field selectors - Type-safe field references for InstanceEmit
- Merge policies - Controlling how repeated updates combine
- Error reporting - Debugging instance construction failures