New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make it so callable JSON schema extra works #6798
Conversation
Deploying with Cloudflare Pages
|
please review |
for field_info in field_infos: | ||
new_kwargs.update(field_info._attributes_set) | ||
for x in field_info.metadata: | ||
if not isinstance(x, FieldInfo): | ||
metadata[type(x)] = x |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ran into a case where this was necessary to get proper merging of FieldInfos that use gt
, lt
, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This stuff is getting really annoying. @samuelcolvin not having to do this stuff is one of the big advantages of the Output enum thing we were discussing the other day.
@@ -1480,6 +1462,37 @@ def json_schema_update_func( | |||
return maybe_updated_schema | |||
return original_schema | |||
|
|||
def apply_single_annotation_json_schema( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had to break this out as a separate function in order to ensure the callable JSON schema ran after all modifications to the core schema that influence the json schema are performed. Otherwise, for example, you can't remove the default
in the callable.
@@ -1493,6 +1506,7 @@ def _get_wrapped_inner_schema( | |||
def new_handler(source: Any) -> core_schema.CoreSchema: | |||
schema = metadata_get_schema(source, get_inner_schema) | |||
schema = self.apply_single_annotation(schema, annotation) | |||
schema = self.apply_single_annotation_json_schema(schema, annotation) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As you can see here, everything related to JSON schema now runs after everything not related to JSON schema.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No typo as far as I can tell — the self.apply_single_annotation
does everything not related to JSON schema, then after that, self.apply_single_annotation_json_schema
does everything related to json schema. I think that's what I said in the comment above?
tests/test_json_schema.py
Outdated
d: Annotated[int, Field(default=4), Field(json_schema_extra=pop_default)] | ||
# TODO: it doesn't work properly to have both annotation and assigned value of FieldInfo: | ||
# e: Annotated[int, Field(json_schema_extra=pop_default)] = Field(default=5) | ||
# f: Annotated[int, Field(default=6)] = Field(json_schema_extra=pop_default) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be possible to make this and the previous case work (i.e., e
and f
), but I tried and it ended up being a significantly bigger change and I wasn't able to see it all the way through and get all tests passing. So rather than wasting time on it for now, I just added a note here. Happy to create an issue for this if there isn't one already. (CC @adriangb)
While it might be tempting to try to make this an error, for various reasons, I think it would be preferable if assigning a FieldInfo
was equivalent to adding it as the last annotation on the field (and that does work, as you can see in the handling of attributes c
and d
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do think we should make it work at some point. Is it not working now? I thought it was.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It works for most things, it just doesn't work for json_schema_extra
and default
specifically, annoyingly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, even on main, this doesn't work:
from pydantic import BaseModel, Field
from typing_extensions import Annotated
class Model(BaseModel):
a: Annotated[int, Field(default=1)] = Field()
Model()
pydantic_core._pydantic_core.ValidationError: 1 validation error for Model
a
Field required [type=missing, input_value={}, input_type=dict]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was able to fix this through an appropriate change in FieldInfo.from_annotated_attribute
💪
Can’t this go in pydantic/pydantic/json_schema.py Line 2174 in 6ef959f
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks fine, I'm a bit lost on a few things but if you're confident, let's merge.
if isinstance(field_info.json_schema_extra, dict): | ||
json_schema_updates.update(field_info.json_schema_extra) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why don't we call field_info.json_schema_extra
if it's a function here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
okay so, in the update func (def json_schema_update_func
below), we have:
json_schema = {**handler(schema), **json_schema_updates}
and handler(schema)
may result in a different schema than just the updates; json_schema_updates
is not the whole json schema. In particular, getting stuff like "type": "object"
(or even other types for RootModel
subclasses), you probably want that to be accessible in the json_schema_extra
call, which is why that needs to happen after the call to handler
in the json_schema_update_func
below.
if callable(field_info.json_schema_extra): | ||
field_info.json_schema_extra(json_schema) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
again, I'm a bit lost why we don't update do json_schema.update(field_info.json_schema_extra)
here if it's a dict?
If you sure these are fine, great.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can clean this up, I'll push a commit doing that shortly
…ds and dataclasses
Closes #6647
Note that as mentioned here, the better solution for FastAPI is to use a custom
GenerateJsonSchema
that generates the JSON schema differently forOptional
query params, etc.. But this at least makes it possible to express what you want on the pydantic side via:Selected Reviewer: @samuelcolvin