---
title: Field selectors
---

**What you'll learn**: How to use type-safe field references instead of strings, when to choose each approach, and how to catch typos at type-check time.

Field selectors let you refer to model fields in a way static type checkers can verify, while the runtime resolves the field name without reflection.

### Why use field selectors?

Field selectors provide three key benefits over plain strings:

1. **IDE autocomplete**: Your editor will suggest available fields as you type
2. **Type-check time errors**: Typos are caught before running your code
3. **Refactoring safety**: Renaming a field updates all selector references automatically

If you're working with plain dicts or don't use type checkers, plain strings work fine.

### What is a field selector?

Use `field_of(Model, lambda m: m.field)` to produce the string field name at runtime. Type checkers validate the lambda, catching typos early.

``` {python}
from etielle.core import field_of

class User:
    id: str
    email: str

print(field_of(User, lambda u: u.email))
```

### Constraints (enforced at runtime)

- Exactly one attribute access must occur.
- No method calls, no indexing, no chained attributes.

``` {python}
from etielle.core import field_of

class Model:
    x: int

# OK
print(field_of(Model, lambda m: m.x))

# Raises ValueError (method call)
try:
    field_of(Model, lambda m: m.x.__str__())
except ValueError:
    pass

# Raises ValueError (chained)
try:
    field_of(Model, lambda m: m.x.real)
except ValueError:
    pass
```

### When to use field selectors vs. strings

Use `field_of()` selectors when:

- Working with Pydantic models, dataclasses, or typed classes
- Using a type checker (mypy, pyright)
- Want IDE autocomplete and refactoring support

Use plain string field names when:

- Working with plain dicts or dynamic data
- Prototyping quickly without types
- Field names are computed dynamically at runtime

### Using selectors in instance emission

Selectors are used with `FieldSpec` inside `InstanceEmit`. They are resolved against the builder’s model.

``` {python}
from etielle.core import MappingSpec, TraversalSpec, field_of
from etielle.transforms import get
from etielle.instances import InstanceEmit, FieldSpec, PydanticBuilder
from pydantic import BaseModel

class UserModel(BaseModel):
    id: str
    email: str

emit = InstanceEmit[UserModel](
    table="users",
    join_keys=[get("id")],
    fields=[
        FieldSpec(selector=field_of(UserModel, lambda u: u.id), transform=get("id")),
        FieldSpec(selector=field_of(UserModel, lambda u: u.email), transform=get("email")),
    ],
    builder=PydanticBuilder(UserModel),
)
```

If you use a builder without a `model` attribute, pass string field names instead of selectors.

### Comparison: Selectors vs. Strings

Here's a side-by-side comparison showing the tradeoffs:

``` {python}
#| eval: false
from etielle.instances import InstanceEmit, FieldSpec, PydanticBuilder
from etielle.core import field_of
from etielle.transforms import get
from pydantic import BaseModel

class User(BaseModel):
    id: str
    email: str

# With field selectors (type-safe):
emit_safe = InstanceEmit[User](
    table="users",
    join_keys=[get("id")],
    fields=[
        FieldSpec(selector=field_of(User, lambda u: u.id), transform=get("id")),
        FieldSpec(selector=field_of(User, lambda u: u.emial), transform=get("email")),  # ❌ Type checker catches typo!
    ],
    builder=PydanticBuilder(User),
)

# With plain strings (more flexible, less safe):
emit_strings = InstanceEmit[User](
    table="users",
    join_keys=[get("id")],
    fields=[
        FieldSpec(selector="id", transform=get("id")),
        FieldSpec(selector="emial", transform=get("email")),  # ⚠️ Caught at runtime only (with strict_fields=True)
    ],
    builder=PydanticBuilder(User),
)
```

### Backwards-compat strings, with strict validation

String field names remain supported. When `strict_fields=True` (default), unknown fields are recorded with helpful suggestions, and you can opt into `strict_mode="fail_fast"` to raise immediately.

``` {python}
#| eval: false
from etielle.transforms import get
from etielle.instances import InstanceEmit, FieldSpec, TypedDictBuilder

emit = InstanceEmit[dict](
    table="users",
    join_keys=[get("id")],
    fields=[
        FieldSpec(selector="emali", transform=get("email")),  # typo on purpose
    ],
    builder=TypedDictBuilder(lambda d: d),
    strict_fields=True,
    # strict_mode="fail_fast",  # enable to raise instead of collect
)
```

### Reference

- `etielle.core.field_of(model, selector)` → `str` field name
- `FieldSpec[T](selector: Callable[[T], Any] | str, transform)`
- Resolved through builders with a `model` (e.g., `PydanticBuilder`).

### See also

- [Instance emission](instance-emission.qmd) - Where field selectors are most commonly used
- [Error reporting](error-reporting.qmd) - How unknown field errors are reported
