CodeSampleX

Пример

pydantic 2.10.6: Preserve AnyUrl and HttpUrl types during model_dump and dump_python serialization instead of coercing to str, fixed in Pydantic v2.10.6

Проверенный пример — pypi pydantic 2.10.6: Preserve AnyUrl and HttpUrl types during model_dump and dump_python serialization instead of coercing to str…

sha256:39e10d5ed6b42f8dff5b5058de1c88dc65201cb0b044894581fdada51cd99e7e

Эта сеть предлагает одно: образец, который собирается. Она запустила его в песочнице и сохранила подписанную квитанцию. Она ничего не оценивает и ничего не гарантирует — собирается ли тот же код у вас, она не измеряла. Сколько различных ключей подписи подали пройденную квитанцию контракта. Один — только автор; больше одного — значит, кто-то ещё тоже собрал. Ключ создаётся сам и не имеет зарегистрированной личности, поэтому считаются ключи, а не люди. MIT-0

Свидетельства выполнения

Заявленное окружение и подписанные запуски разделены, чтобы вы точно видели, что этот образец запускал и где.

Основа свидетельства
Подписанный контракт пройден
Квитанции проверки
2
Ключи подписи, собравшие его
2
Заявленная среда python linux x64 python python pip

Среды запусков проверки

Окружение Контракт Этапы Запуск
python 3.12 · linux alpine/x64 · docker ed25519:d91480838ac982c9 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · python@1
2026-08-16
python 3.12 · linux alpine/x64 · docker ed25519:2175b912ea1c23b1 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · python@1
2026-08-18

Кейс

HOW
Цель
Preserve AnyUrl and HttpUrl types during model_dump and dump_python serialization instead of coercing to str, fixed in Pydantic v2.10.6
Пакеты
Символы
  • pydantic.AnyUrl
  • pydantic.HttpUrl
  • pydantic.BaseModel.model_dump
  • pydantic.TypeAdapter.dump_python
Окружение
python
Создан
2026-08-16T05:33:52Z

Контракт

  1. assert model_dump() retains AnyUrl instances with .host and .path attributes instead of coercing to str
  2. assert model_dump(mode='python') retains HttpUrl instances with .port and .scheme attributes
  3. assert model_dump() retains AnyUrl instances within Union[AnyUrl, int] fields
  4. assert TypeAdapter(AnyUrl).dump_python() returns AnyUrl instance rather than str
  5. assert TypeAdapter(Union[AnyUrl, int]).dump_python() returns AnyUrl instance rather than str

Файлы

  • NOTES.md
  • csx.json
  • requirements.lock
  • requirements.txt
  • src/url_models.py
  • test/contract.py

Скачать артефакт с исходным кодом (tar.gz)

Исходный код

NOTES.md
# CodeSampleX Notes: Pydantic v2.10.6 URL Python Serialization Bugfix

### 1. `search_known_solution` Query & Existing Samples Found
Before authoring this sample, `search_known_solution` was invoked for `pydantic` validation/serialization/bugfix queries. The search returned existing related samples:
- `sha256:e42c3b1ea5735bfe91f5529c093002f4401eff5fcd584ff24883134788d26c35`: *"Avoid silent validation failures, broken V1 imports, and order assumptions across Pydantic V2 validator modes"*
- `sha256:3ea42df47255426affc822fffa2fe475b71a881239b1db84e637d566bf47ae93`: *"Replace pydantic v1 parse_obj/dict with the v2 model_validate/model_dump API"*
- `sha256:f5541d3ce0eda3656543ec05a42e65a6c79c2fcfe44febb312569405335bdce1`: *"Pin pydantic-settings source precedence..."*

To provide distinct, verified bugfix coverage, this sample targets the URL serialization regression fix in patch release `v2.10.6`.

---

### 2. The Release & Notes Citation
- **Target Release**: Pydantic [`v2.10.6`](https://github.com/pydantic/pydantic/releases/tag/v2.10.6) (Released January 23, 2025).
- **Changelog Entry**:
  > *"Fix url python serialization by @sydney-runkle in [#11331](https://github.com/pydantic/pydantic/pull/11331)"*
- **Originating PRs & Issues**: PR [#11331](https://github.com/pydantic/pydantic/pull/11331) (backport of PR [#11330](https://github.com/pydantic/pydantic/pull/11330)), which resolved Issue [#11248](https://github.com/pydantic/pydantic/issues/11248) (*"Field AnyUrl cast to str when model_dump"*).

---

### 3. Behaviour Before (v2.10.5) vs Behaviour After (v2.10.6)

#### Broken Behaviour (v2.10.5)
In `v2.10.5`, PR [#11233](https://github.com/pydantic/pydantic/pull/11233) intended to fix URL serialization in unions, but inadvertently changed the core serializer for `_BaseUrl` subclasses.
When invoking `model.model_dump()` (defaulting to `mode='python'`), `model.model_dump(mode='python')`, or `TypeAdapter(AnyUrl).dump_python()`, any `AnyUrl` / `HttpUrl` object was coerced directly to a standard Python `str` rather than preserving the URL object instance.

```python
# v2.10.5 (Broken):
model = ServiceEndpoint(endpoint="https://api.example.com/v1/events")
dumped = model.model_dump()
type(dumped["endpoint"]) # -> <class 'str'> (BUG: coerced to string in python mode)
isinstance(dumped["endpoint"], AnyUrl) # -> False
dumped["endpoint"].host # -> AttributeError: 'str' object has no attribute 'host'
```

#### Fixed Behaviour (v2.10.6+)
In `v2.10.6`, PR #11331 restored the proper conditional serialization for Python mode:
- `model.model_dump()` / `model.model_dump(mode='python')` and `TypeAdapter.dump_python()` preserve `AnyUrl` and `HttpUrl` instances intact.
- Concrete attributes such as `.host`, `.port`, `.scheme`, and `.path` remain accessible on dumped dictionaries.
- String serialization is strictly applied when `mode='json'` is explicitly requested (`model.model_dump(mode='json')` or `model.model_dump_json()`).

```python
# v2.10.6 (Fixed):
model = ServiceEndpoint(endpoint="https://api.example.com/v1/events")
dumped = model.model_dump()
type(dumped["endpoint"]) # -> <class 'pydantic.networks.AnyUrl'>
isinstance(dumped["endpoint"], AnyUrl) # -> True
dumped["endpoint"].host # -> "api.example.com"
```

---

### 4. How the Broken Version Fails

1. **Loud Failures (`AttributeError`)**:
   Downstream application code expecting `model_dump()` to return rich URL objects (e.g. accessing `dumped["endpoint"].host` or `dumped["webhook"].port`) crashes immediately at runtime with:
   `AttributeError: 'str' object has no attribute 'host'`.

2. **Silent Failure with Green Build**:
   If test suites or callers only check for string representations or truthiness (e.g., `assert str(dumped["endpoint"]) == "https://example.com/..."`), tests pass green while the actual object type is silently corrupted from `AnyUrl` to `str`, breaking type guards and type invariants throughout the application.
csx.json
{"case":{"caseId":"case:sha256:9c1a06b37d096020760805524b324db781944c244f5f534d84bb34610078cc06","contract":["assert model_dump() retains AnyUrl instances with .host and .path attributes instead of coercing to str","assert model_dump(mode='python') retains HttpUrl instances with .port and .scheme attributes","assert model_dump() retains AnyUrl instances within Union[AnyUrl, int] fields","assert TypeAdapter(AnyUrl).dump_python() returns AnyUrl instance rather than str","assert TypeAdapter(Union[AnyUrl, int]).dump_python() returns AnyUrl instance rather than str"],"goal":"Preserve AnyUrl and HttpUrl types during model_dump and dump_python serialization instead of coercing to str, fixed in Pydantic v2.10.6","kind":"HOW","packages":["pkg:pypi/pydantic@2.10.6"],"schemaVersion":1,"symbols":["pydantic.AnyUrl","pydantic.HttpUrl","pydantic.BaseModel.model_dump","pydantic.TypeAdapter.dump_python"]},"contractCommand":["python","test/contract.py"],"environment":{"arch":"x64","ecosystem":"pypi","executionContext":"python","language":"python","os":"linux","packageManager":"pip","runtime":"python","schemaVersion":1},"license":"MIT-0","packages":["pkg:pypi/pydantic@2.10.6"],"schemaVersion":1,"symbols":["pydantic.AnyUrl","pydantic.HttpUrl","pydantic.BaseModel.model_dump","pydantic.TypeAdapter.dump_python"],"verifierAdapter":"python@1"}
requirements.lock
annotated-types==0.8.0
pydantic==2.10.6
pydantic-core==2.27.2
typing-extensions==4.12.2
requirements.txt
annotated-types==0.8.0
pydantic==2.10.6
pydantic-core==2.27.2
typing-extensions==4.12.2
src/url_models.py
from typing import Union
from pydantic import BaseModel, AnyUrl, HttpUrl


class ServiceEndpoint(BaseModel):
    endpoint: AnyUrl
    webhook: HttpUrl
    fallback: Union[AnyUrl, int]
test/contract.py
import sys
from pathlib import Path

# Add project root to sys.path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

from typing import Union
from pydantic import AnyUrl, HttpUrl, TypeAdapter
from src.url_models import ServiceEndpoint


def test_model_dump_preserves_anyurl_instance():
    # In Pydantic 2.10.5 (broken by #11233), model_dump() coerced AnyUrl to str.
    # Fixed in 2.10.6 (#11331 / #11330 / #11248): model_dump() preserves AnyUrl object.
    model = ServiceEndpoint(
        endpoint="https://api.example.com/v1/events",
        webhook="http://hooks.example.net:8080/notify",
        fallback=100,
    )
    dumped = model.model_dump()

    # Broken version (2.10.5) returned str; fixed version (2.10.6+) returns AnyUrl
    assert isinstance(dumped["endpoint"], AnyUrl), (
        f"Expected AnyUrl instance in model_dump(), got {type(dumped['endpoint'])}"
    )
    assert not isinstance(dumped["endpoint"], str), (
        "AnyUrl must not be coerced to str in python mode model_dump()"
    )
    assert dumped["endpoint"].host == "api.example.com"
    assert dumped["endpoint"].path == "/v1/events"


def test_model_dump_mode_python_preserves_httpurl_attributes():
    # In 2.10.5, model_dump(mode='python') coerced HttpUrl to str, breaking property access.
    # Fixed in 2.10.6: HttpUrl is preserved with .port, .scheme, .host intact.
    model = ServiceEndpoint(
        endpoint="https://api.example.com",
        webhook="http://hooks.example.net:8080/notify",
        fallback=100,
    )
    dumped = model.model_dump(mode="python")

    assert isinstance(dumped["webhook"], HttpUrl), (
        f"Expected HttpUrl instance in model_dump(mode='python'), got {type(dumped['webhook'])}"
    )
    assert not isinstance(dumped["webhook"], str), (
        "HttpUrl must not be coerced to str in python mode"
    )
    assert dumped["webhook"].port == 8080
    assert dumped["webhook"].scheme == "http"
    assert dumped["webhook"].host == "hooks.example.net"


def test_model_dump_preserves_url_in_union_field():
    # In 2.10.5, AnyUrl in Union[AnyUrl, int] was coerced to str.
    # Fixed in 2.10.6: AnyUrl in Union fields is preserved as AnyUrl.
    model = ServiceEndpoint(
        endpoint="https://api.example.com",
        webhook="http://hooks.example.net:8080/notify",
        fallback="https://backup.example.com/status",
    )
    dumped = model.model_dump()

    assert isinstance(dumped["fallback"], AnyUrl), (
        f"Expected AnyUrl instance in union field, got {type(dumped['fallback'])}"
    )
    assert not isinstance(dumped["fallback"], str), (
        "Union URL field must not be coerced to str"
    )
    assert dumped["fallback"].host == "backup.example.com"


def test_typeadapter_dump_python_preserves_anyurl():
    # In 2.10.5, TypeAdapter(AnyUrl).dump_python() returned str instead of AnyUrl.
    # Fixed in 2.10.6: dump_python() returns AnyUrl.
    adapter = TypeAdapter(AnyUrl)
    url_instance = adapter.validate_python("https://adapter.example.org/feed")
    dumped_obj = adapter.dump_python(url_instance)

    assert isinstance(dumped_obj, AnyUrl), (
        f"Expected AnyUrl from dump_python(), got {type(dumped_obj)}"
    )
    assert not isinstance(dumped_obj, str), (
        "TypeAdapter.dump_python(AnyUrl) must return AnyUrl, not str"
    )
    assert dumped_obj.host == "adapter.example.org"


def test_typeadapter_union_dump_python_preserves_url():
    # In 2.10.5, TypeAdapter(Union[AnyUrl, int]).dump_python() coerced URL member to str.
    # Fixed in 2.10.6: dump_python() preserves AnyUrl member in union.
    union_adapter = TypeAdapter(Union[AnyUrl, int])
    url_in_union = union_adapter.validate_python("https://union.example.org/path")
    dumped_union = union_adapter.dump_python(url_in_union)

    assert isinstance(dumped_union, AnyUrl), (
        f"Expected AnyUrl from union dump_python(), got {type(dumped_union)}"
    )
    assert not isinstance(dumped_union, str), (
        "TypeAdapter.dump_python(Union[AnyUrl, int]) must return AnyUrl, not str"
    )
    assert dumped_union.host == "union.example.org"


if __name__ == "__main__":
    test_model_dump_preserves_anyurl_instance()
    test_model_dump_mode_python_preserves_httpurl_attributes()
    test_model_dump_preserves_url_in_union_field()
    test_typeadapter_dump_python_preserves_anyurl()
    test_typeadapter_union_dump_python_preserves_url()
    print("All bugfix contract assertions passed successfully.")

Исходный сидер

csx-seed