Summary
On anthropic==0.121.0, passing output_format= to the beta messages resource emits
DeprecationWarning: The 'output_format' parameter is deprecated. Please use 'output_config.format' instead.
— resources/beta/messages/messages.py:4229
but output_config.format cannot accept a pydantic model, which is the input form
output_format= exists for. Following the warning literally raises TypeError, and the
natural workaround (hand it Model.model_json_schema() as a dict) silently skips
transform_schema — so migrating changes the schema that goes on the wire.
Repro
Executed as written against anthropic==0.121.0 / pydantic==2.13.4, with
httpx.Client.send patched to capture the request body and return a 401 (no API key,
nothing leaves the machine).
class Item(BaseModel):
code: str = Field(pattern=r'^[A-Z]{3}$', min_length=3)
A — the deprecated form. Warns, and transform_schema runs:
{"type":"object","title":"Item",
"properties":{"code":{"type":"string","title":"Code",
"description":"{minLength: 3, pattern: ^[A-Z]{3}$}"}},
"additionalProperties":false,"required":["code"]}
B — the migration the warning names. output_config={"format": Item}:
TypeError: Object of type ModelMetaclass is not JSON serializable
C — the workaround, output_config={"format": {"type":"json_schema","schema": Item.model_json_schema()}}:
{"properties":{"code":{"minLength":3,"pattern":"^[A-Z]{3}$","title":"Code","type":"string"}},
"required":["code"],"title":"Item","type":"object"}
C is not A. additionalProperties: false is gone, and minLength/pattern are no
longer folded into description. So a user who follows the deprecation notice stops
getting the normalization the SDK applies today, with no error and no warning.
Cause
transform_schema runs only on the elif branch of
if is_dict(output_format): # beta messages.py:1769, :3778
transformed_output_format = cast(...) # cast through, untransformed
elif is_given(output_format) and output_format is not None:
schema = adapted_type.json_schema()
transformed_output_format = JSONOutputFormatParam(schema=transform_schema(schema), ...)
output_config has no equivalent branch at all — its format is forwarded as given, so
every form of it is untransformed. Measured across create / parse / stream and both
the beta and non-beta resources: output_config.format never transforms.
Two smaller notes from the same measurement:
- The DeprecationWarning fires on the beta resource only.
messages.parse,
messages.stream and messages.count_tokens on the non-beta resource accept
output_format= silently, so the two resources disagree about whether this parameter
is deprecated.
count_tokens(output_format=<dict>) is also untransformed while
count_tokens(output_format=<model>) is transformed, so a token count taken via the
dict form is counting a different document than the one parse() would send.
Suggested shape of a fix
Either route output_config.format through the same branch (accept a type there and
transform it), or state in the deprecation message that output_config.format takes a
pre-built json_schema dict and applies no transform — the current wording implies a
drop-in rename, and it is not one.
Test-design warning
A regression test that asserts only "the request was built" cannot see this — the
request builds fine in both A and C. It has to assert on the emitted schema. And a
fixture whose model carries no constrained fields is weaker than it looks: it will still
differ on additionalProperties, but nothing else, so the demotion half of the change
goes untested. Use a field with pattern=/min_length= as above.
Scope
No API key was used, so this is entirely about the request the SDK builds; I am not
claiming anything about how the service treats either payload.
Summary
On
anthropic==0.121.0, passingoutput_format=to the beta messages resource emitsbut
output_config.formatcannot accept a pydantic model, which is the input formoutput_format=exists for. Following the warning literally raisesTypeError, and thenatural workaround (hand it
Model.model_json_schema()as a dict) silently skipstransform_schema— so migrating changes the schema that goes on the wire.Repro
Executed as written against
anthropic==0.121.0/pydantic==2.13.4, withhttpx.Client.sendpatched to capture the request body and return a 401 (no API key,nothing leaves the machine).
A — the deprecated form. Warns, and
transform_schemaruns:{"type":"object","title":"Item", "properties":{"code":{"type":"string","title":"Code", "description":"{minLength: 3, pattern: ^[A-Z]{3}$}"}}, "additionalProperties":false,"required":["code"]}B — the migration the warning names.
output_config={"format": Item}:C — the workaround,
output_config={"format": {"type":"json_schema","schema": Item.model_json_schema()}}:{"properties":{"code":{"minLength":3,"pattern":"^[A-Z]{3}$","title":"Code","type":"string"}}, "required":["code"],"title":"Item","type":"object"}C is not A.
additionalProperties: falseis gone, andminLength/patternare nolonger folded into
description. So a user who follows the deprecation notice stopsgetting the normalization the SDK applies today, with no error and no warning.
Cause
transform_schemaruns only on theelifbranch ofoutput_confighas no equivalent branch at all — itsformatis forwarded as given, soevery form of it is untransformed. Measured across
create/parse/streamand boththe beta and non-beta resources:
output_config.formatnever transforms.Two smaller notes from the same measurement:
messages.parse,messages.streamandmessages.count_tokenson the non-beta resource acceptoutput_format=silently, so the two resources disagree about whether this parameteris deprecated.
count_tokens(output_format=<dict>)is also untransformed whilecount_tokens(output_format=<model>)is transformed, so a token count taken via thedict form is counting a different document than the one
parse()would send.Suggested shape of a fix
Either route
output_config.formatthrough the same branch (accept a type there andtransform it), or state in the deprecation message that
output_config.formattakes apre-built
json_schemadict and applies no transform — the current wording implies adrop-in rename, and it is not one.
Test-design warning
A regression test that asserts only "the request was built" cannot see this — the
request builds fine in both A and C. It has to assert on the emitted schema. And a
fixture whose model carries no constrained fields is weaker than it looks: it will still
differ on
additionalProperties, but nothing else, so the demotion half of the changegoes untested. Use a field with
pattern=/min_length=as above.Scope
No API key was used, so this is entirely about the request the SDK builds; I am not
claiming anything about how the service treats either payload.