anthropics / anthropics/anthropic-sdk-python

transform_schema empties a patternProperties map: field can only ever be {}

Đang mở
#1,817 0 bình luận 0 reaction 0 người được giao Xem trên GitHub
Ngôn ngữ chính
Python
Star
3.9k
Fork
853
Merge trung bình
1 ngày 11 giờ
Pull request đã merge (30 ngày)
10

Mô tả

### Summary

`transform_schema` turns a dict-shaped field whose keys are described by `patternProperties` (or `propertyNames` / `unevaluatedProperties`) into an object whose **only legal value is `{}`**. The request builds, the API returns 200, and the field can never be populated. Nothing raises and nothing warns.

This is not the same thing as the demote-to-prose policy working as intended — see "why this isn't just a demotion" below. It reproduces from ordinary Pydantic output, on `anthropic==0.121.0`.

### Reproduction

```py
from typing import Dict
from typing_extensions import Annotated
from pydantic import BaseModel, StringConstraints
from anthropic.lib._parse._transform import transform_schema

class M(BaseModel):
m: Dict[Annotated[str, StringConstraints(pattern=r'^S_')], str]

field = M.model_json_schema()["properties"]["m"]
# {'patternProperties': {'^S_': {'type': 'string'}}, 'title': 'M', 'type': 'object'}

print(transform_schema(field))
# {'type': 'object', 'title': 'M', 'properties': {}, 'additionalProperties': False,
# 'description': "{patternProperties: {'^S_': {'type': 'string'}}}"}
```

`properties: {}` plus `additionalProperties: False` admits exactly one instance: `{}`. The model is being asked for a dictionary it is structurally forbidden from filling.

Same result for the `Optional[...]` spelling, which Pydantic renders as an `anyOf` with the map in one branch:

```py
m: Optional[Dict[Annotated[str, StringConstraints(pattern=r'^S_')], str]] = None
# -> {'anyOf': [{'type': 'object', 'properties': {}, 'additionalProperties': False,
# 'description': "{patternProperties: ...}"}, {'type': 'null'}], ...}
```

### Mechanism

`lib/_parse/_transform.py:129-133` (0.121.0) — the object branch:

```py
strict_schema["properties"] = { # absent `properties` -> {} via pop's default
key: transform_schema(p) for key, p in json_schema.pop("properties", {}).items()
}
json_schema.pop("additionalProperties", None)
strict_schema["additionalProperties"] = False
```

`patternProperties` is not consulted here, so it falls through to the leftover-stringify pass and lands in `description`. Both steps are individually defensible; the node is emptied by their combination.

### Why this isn't just a demotion

Demotion normally **widens**: the constraint stops being enforced and the field keeps working. That is exactly what happens when the node *also* declares `properties`:

```py
{"type": "object", "properties": {"a": {"type": "string"}}, "required": ["a"],
"patternProperties": {"^S_": {"type": "string"}}}
# -> properties preserved, patternProperties demoted to description. Field still works.
```

When one of these keywords is the node's **only** way of admitting a key, the same demotion plus the forced close **narrows the field to nothing**. So the honest description of the outcome flips depending on whether `properties` is present — and the failing case is the one that produces no signal at all.

### Scope (measured 2026-08-10, with controls)

`patternProperties`, `propertyNames` and `unevaluatedProperties` all reach the emptying; an ordinary closed object and a node carrying `properties` are unaffected, which is the control that the harness discriminates.

Worth knowing because it suggests a shared design rather than a Python-only slip: `@anthropic-ai/sdk@0.116.0` and `anthropic-sdk-go@v1.62.0` produce the identical emptied node for these keywords. That is a **different** split from a plain `additionalProperties` map, which the Go SDK *preserves* — `transformSchema` has an explicit dictionary clause, but it keys on `additionalProperties`, so it does nothing for these three spellings. If the dictionary clause is considered the intended behaviour for maps, these keywords are simply outside it.

Reachability, stated precisely: only `patternProperties` is emitted by Pydantic (the pattern-keyed dict above). `propertyNames` alone is a hand-authored / OpenAPI-3.1 shape — `zod`'s `z.record()` emits it beside `additionalProperties`, which takes the ordinary map path.

### Suggestion

I don't think there is a lossless repair — structured outputs has no way to express "keys matching a pattern", so any fix is a choice about which failure the caller gets. Two options that both seem better than the current one:

1. **Raise**, the way the empty-`type`-array case is now handled in #1813. A `ValueError` naming the keyword is actionable; a silently unsatisfiable field is not.
2. **Preserve the map** by treating a `patternProperties`-only node the way the Go SDK treats an `additionalProperties`-only node, if the API tolerates it. That needs an API-side answer I can't get offline.

A regression test for either should assert on the **output shape**, not on accept/reject: the current behaviour is accepted by everything, so acceptance can't see this bug — being accepted is precisely what goes wrong.

All of the above is checkable offline; `transform_schema` needs no API key.

### Relationship to #1813

Independent of it — the emptying reproduces identically before and after that PR for every Pydantic-emitted shape, so it is **pre-existing and not caused by it**. One small interaction worth noting for whoever merges: the list-form spelling `{"type": ["object", "null"], "patternProperties": {...}}` currently raises `AssertionError` and after #1813 it builds successfully as an emptied field, i.e. it joins this bug. That spelling isn't Pydantic-emitted (Pydantic uses `anyOf` for `Optional`), so it's hand-authored input only — not a reason to hold the PR, which fixes what it says it fixes.

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Hướng nghiên cứu

Read lib/_parse/_transform.py around lines 129-133 and run the offline Pydantic reproduction from the issue. Confirm the output shape for patternProperties-only, propertyNames-only, and unevaluatedProperties-only schemas, then check with maintainers whether the intended outcome is an actionable ValueError or map preservation. Add a regression test asserting the selected output shape, with completion defined by eliminating the silently unsatisfiable schema.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Đánh giá

Công nghệ
python
Lĩnh vực
api
Loại issue
Lỗi
Độ khó
3/5
Thời gian dự kiến
1-2 ngày
Mức độ hoạt động
Ít trao đổi
Độ rõ ràng
Khá rõ ràng
Mức phù hợp với người mới
52/100

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.