Skip to content

Use Pydantic's alias_generator instead of a custom CamelCaseField - #348

Merged
Bernhard Merkle (bmerkle) merged 7 commits into
microsoft:mainfrom
gvanrossum:aliases
Oct 19, 2026
Merged

Bernhard Merkle (bmerkle) merged 7 commits into
microsoft:mainfrom
gvanrossum:aliases

Conversation

@gvanrossum

Copy link
Copy Markdown
Collaborator

Written by Claude and ChatGPT.
Includes tests.

This also fixes and tests some pre-existing issues where certain descriptions didn't make it to the TypeScript schema generated by TypeChat. TypeChat doesn't render Field descriptions (it knows nothing about pydantic.Field), but it does know about Annotated[T, Doc("...")], and renders that doc string as a TypeScript comment.

Things to know about TypeChat:

  • It ignores anything in a Field(...), whether used as initializer or via Annotated[T, Field(...)]
  • It assumes a field is optional iff it has an initial value, even if that initial value is a Field(...).
  • It only uses Annotated[T, Doc("...")] to generate field comments in the TypeScript schema.

All models now accept both camelCase and snake_case field names when
loading from data. They always dump camelCase field names.

File field_helpers.py is no longer needed, and is removed.

This patch was generated by Claude; I skimmed it.

@bmerkle Bernhard Merkle (bmerkle) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice cleanup: swapping the descriptor hack for AliasGenerator is much more robust. Full suite passes locally (811 passed, pyright clean), and the generated TypeChat schemas are identical to main except for the four intended Action description comments.

A few notes, mostly about docs:

Comment thread tests/test_dataclasses.py
Comment on lines +22 to +27
def dump(obj: object) -> dict:
return obj.__pydantic_serializer__.to_python(obj, by_alias=True) # type: ignore[attr-defined]


def load[T](cls: type[T], data: dict) -> T:
return cls.__pydantic_validator__.validate_python(data) # type: ignore[attr-defined]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

route the message tests through serialize_object/deserialize_object instead of pydantic_serializer/pydantic_validator, so they follow the path persisted data takes

Comment thread src/typeagent/knowpro/dataclasses.py Outdated
Comment on lines +5 to +8
Fields are declared in snake_case. On input, both the snake_case name and
its camelCase form are accepted; on output (``by_alias=True``), camelCase is
written. An explicit alias on a field takes precedence.
"""

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The module docstring is wrong for fields with explicit aliases .
"Both the snake_case name and its camelCase form are accepted" does not hold once a field sets its own alias:

  • With Field(alias="X"), only X is accepted.
  • With only serialization_alias= set, camelCase input is silently ignored, so the field doesn't round-trip.

Comment thread CHANGELOG.md Outdated
decorator now gives every field a camelCase alias. As a result, the
`source_id` field of messages and the `src_url` field of `EmailMessage` are
now saved as `sourceId` and `srcUrl`; data saved under the old names still
loads.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CHANGELOG lists far fewer renamed fields than actually change.
It names only source_id and src_url. Comparing every pydantic dataclass on main against the PR, about 40 more multi-word fields now dump as camelCase when serialized with by_alias=True. Among them:

  • KnowledgeResponse.inverse_actions, AnswerResponse.why_no_answer, DateTimeRange.start_date/stop_date
  • SearchFilter., ActionTerm., WhenFilter., SearchOptions.
  • AddMessagesResult., ChunkFailure., ConversationMetadata.*, ScoredMessageOrdinal.message_ordinal

Within the package only messages are persisted, so "saved as" is correct only for those two fields. Any other caller that serializes these classes will see the new keys. A one-line catch-all would cover it.

Comment thread src/typeagent/knowpro/dataclasses.py Outdated

@bmerkle Bernhard Merkle (bmerkle) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, all my earlier points are addressed. The new tests pin down the explicit-alias and serialization-alias-only behaviour nicely. CI is green, and I also ran the alias/serialization tests against pydantic 2.12 on py3.12 (all pass).

On the 2.11 question: the 2.10 floor is already unreachable in practice, because pydantic-ai-slim>=2.25.0 requires pydantic>=2.12 (uv refuses to resolve pydantic==2.10.* against the package). So I'm fine with raising the declared floor.

LGTM

@bmerkle
Bernhard Merkle (bmerkle) merged commit adab552 into microsoft:main Oct 19, 2026
21 checks passed
Bernhard Merkle (bmerkle) pushed a commit that referenced this pull request Oct 19, 2026
## Summary

- `CompiledSearchTerm` was a stdlib `@dataclass` subclassing the
pydantic dataclass `SearchTerm`. A stdlib dataclass takes the base
class's class-level value as the field default, and for `related_terms`
that value is a pydantic `FieldInfo` object, not `None`.
- `CompiledSearchTerm(Term("x")).related_terms` was therefore a truthy
`FieldInfo`, and `dedupe_related_terms` crashed on it with
`AttributeError: 'FieldInfo' object has no attribute 'text'`.
- Production code only creates `CompiledSearchTerm` via `cast`, so it
isn't hit there today; tests work around it by always passing
`related_terms=None`.
- Declare `CompiledSearchTerm` with typeagent's pydantic `dataclass`
decorator, like `SearchTerm` (same import as `universal_message.py`).
- A scan of all 103 dataclasses in the package found no other
non-pydantic dataclass inheriting a `FieldInfo` default.

Relation to #348: #348 changes this default to `Field(default=None,
...)`, which is still a `FieldInfo`, so the bug remains there. This PR
doesn't touch any file #348 changes; it applies cleanly on top of #348
and the affected tests pass there too.

## Validation

- New tests: a default-constructed `CompiledSearchTerm` has
`related_terms is None`, and `dedupe_related_terms` accepts such terms.
Both fail without the fix.
- make format check test

Co-authored-by: wtfyd-agent[bot] <328989958+wtfyd-agent[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants