Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
fbc5c9c
first step, removing CoreMetadataHandler construct - type checking va…
sydney-runkle Oct 21, 2024
bd181f1
remove reliance on build_metadata_dict
sydney-runkle Oct 21, 2024
f9c346d
standardizing dict updates and json schema extra
sydney-runkle Oct 21, 2024
a1442ff
removing as much json schema logic as possible from _generate_schema.…
sydney-runkle Oct 22, 2024
f93552c
minor docs fixes + revert enum change to fix tests
sydney-runkle Oct 22, 2024
9d8f4a4
Add some casting + standardize update methods so that CoreMetadata of…
sydney-runkle Oct 22, 2024
e3b7426
remove deprecated internal function
sydney-runkle Oct 22, 2024
a1ebd03
adding warnings for lack of mixed composure support for json_schema_e…
sydney-runkle Oct 23, 2024
41bc424
typing fix for 3.13
sydney-runkle Oct 23, 2024
7e479fe
fix circular reference issue
sydney-runkle Oct 23, 2024
aea1c00
add list of failing tests
sydney-runkle Oct 23, 2024
d8429f1
maybe this is the way you skip that test?
sydney-runkle Oct 23, 2024
88f9247
PR feedback
sydney-runkle Oct 25, 2024
b572456
root description fix
sydney-runkle Oct 25, 2024
7a17254
Merge branch 'main' into metadata-stuck-on-a-flight
sydney-runkle Oct 25, 2024
49e75eb
pr feedback re typing
sydney-runkle Oct 28, 2024
8d09bc7
Merge branch 'metadata-stuck-on-a-flight' of https://github.com/pydan…
sydney-runkle Oct 28, 2024
d66ae3b
import fix
sydney-runkle Oct 28, 2024
5f8b5ba
deselect multiple
sydney-runkle Oct 28, 2024
2a08ddf
fix test skip formatting
sydney-runkle Oct 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 3 additions & 37 deletions docs/concepts/json_schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -650,43 +650,9 @@ print(json.dumps(ta.json_schema(), indent=2))
"""
```

If you would prefer for the last of your `json_schema_extra` specifications to override the previous ones,
you can use a `callable` to make more significant changes, including adding or removing keys, or modifying values.
You can use this pattern if you'd like to mimic the behavior of the `json_schema_extra` overrides present
in Pydantic v2.8 and earlier:

```py
import json

from typing_extensions import Annotated, TypeAlias

from pydantic import Field, TypeAdapter
from pydantic.json_schema import JsonDict

ExternalType: TypeAlias = Annotated[
int, Field(json_schema_extra={'key1': 'value1', 'key2': 'value2'})
]


def finalize_schema(s: JsonDict) -> None:
s.pop('key1')
s['key2'] = s['key2'] + '-final'
s['key3'] = 'value3-final'


ta = TypeAdapter(
Annotated[ExternalType, Field(json_schema_extra=finalize_schema)]
)
print(json.dumps(ta.json_schema(), indent=2))
"""
{
"key2": "value2-final",
"key3": "value3-final",
"type": "integer"
}
"""
```

!!! note
We no longer (and never fully did) support composing a mix of `dict` and `callable` type `json_schema_extra` specifications.
If this is a requirement for your use case, please [open a pydantic issue](https://github.com/pydantic/pydantic/issues/new/choose) and explain your situation - we'd be happy to reconsider this decision when presented with a compelling case.
Comment thread
sydney-runkle marked this conversation as resolved.

### `WithJsonSchema` annotation

Expand Down
2 changes: 2 additions & 0 deletions docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ make docs
# You can also use `pdm run mkdocs serve` to serve the documentation at localhost:8000
```

If this isn't working due to issues with the imaging plugin, try commenting out the `social` plugin line in `mkdocs.yml` and running `make docs` again.

#### Updating the documentation

We push a new version of the documentation with each minor release, and we push to a `dev` path with each commit to `main`.
Expand Down
137 changes: 74 additions & 63 deletions pydantic/_internal/_core_metadata.py
Original file line number Diff line number Diff line change
@@ -1,84 +1,95 @@
from __future__ import annotations as _annotations

import typing
from typing import Any, cast
from typing import TYPE_CHECKING, Any, TypedDict, cast
from warnings import warn

import typing_extensions

if typing.TYPE_CHECKING:
if TYPE_CHECKING:
from pydantic_core import CoreSchema

from ..config import JsonDict, JsonSchemaExtraCallable
from ._schema_generation_shared import (
CoreSchemaOrField,
GetJsonSchemaFunction,
)


class CoreMetadata(typing_extensions.TypedDict, total=False):
class CoreMetadata(TypedDict, total=False):
"""A `TypedDict` for holding the metadata dict of the schema.

Attributes:
pydantic_js_functions: List of JSON schema functions.
pydantic_js_functions: List of JSON schema functions that resolve refs during application.
pydantic_js_annotation_functions: List of JSON schema functions that don't resolve refs during application.
pydantic_js_prefer_positional_arguments: Whether JSON schema generator will
prefer positional over keyword arguments for an 'arguments' schema.
"""

pydantic_js_functions: list[GetJsonSchemaFunction]
pydantic_js_annotation_functions: list[GetJsonSchemaFunction]

# If `pydantic_js_prefer_positional_arguments` is True, the JSON schema generator will
# prefer positional over keyword arguments for an 'arguments' schema.
pydantic_js_prefer_positional_arguments: bool | None
pydantic_js_input_core_schema: CoreSchema | None
pydantic_js_input_core_schema: Schema associated with the input value for the associated
custom validation function. Only applies to before, plain, and wrap validators.
pydantic_js_udpates: key / value pair updates to apply to the JSON schema for a type.
pydantic_js_extra: WIP, either key/value pair updates to apply to the JSON schema, or a custom callable.

TODO: Perhaps we should move this structure to pydantic-core. At the moment, though,
it's easier to iterate on if we leave it in pydantic until we feel there is a semi-stable API.

class CoreMetadataHandler:
"""Because the metadata field in pydantic_core is of type `Dict[str, Any]`, we can't assume much about its contents.

This class is used to interact with the metadata field on a CoreSchema object in a consistent way throughout pydantic.

TODO: We'd like to refactor the storage of json related metadata to be more explicit, and less functionally oriented.
This should make its way into our v2.10 release. It's inevitable that we need to store some json schema related information
TODO: It's unfortunate how functionally oriented JSON schema generation is, especially that which occurs during
the core schema generation process. It's inevitable that we need to store some json schema related information
on core schemas, given that we generate JSON schemas directly from core schemas. That being said, debugging related
issues is quite difficult when JSON schema information is disguised via dynamically defined functions.
"""

__slots__ = ('_schema',)

def __init__(self, schema: CoreSchemaOrField):
self._schema = schema

metadata = schema.get('metadata')
if metadata is None:
schema['metadata'] = CoreMetadata() # type: ignore
elif not isinstance(metadata, dict):
raise TypeError(f'CoreSchema metadata should be a dict; got {metadata!r}.')

@property
def metadata(self) -> CoreMetadata:
"""Retrieves the metadata dict from the schema, initializing it to a dict if it is None
and raises an error if it is not a dict.
"""
metadata = self._schema.get('metadata')
if metadata is None:
self._schema['metadata'] = metadata = CoreMetadata() # type: ignore
if not isinstance(metadata, dict):
raise TypeError(f'CoreSchema metadata should be a dict; got {metadata!r}.')
return cast(CoreMetadata, metadata)


def build_metadata_dict(
*, # force keyword arguments to make it easier to modify this signature in a backwards-compatible way
js_functions: list[GetJsonSchemaFunction] | None = None,
js_annotation_functions: list[GetJsonSchemaFunction] | None = None,
js_prefer_positional_arguments: bool | None = None,
js_input_core_schema: CoreSchema | None = None,
) -> dict[str, Any]:
"""Builds a dict to use as the metadata field of a CoreSchema object in a manner that is consistent with the `CoreMetadataHandler` class."""
metadata = CoreMetadata(
pydantic_js_functions=js_functions or [],
pydantic_js_annotation_functions=js_annotation_functions or [],
pydantic_js_prefer_positional_arguments=js_prefer_positional_arguments,
pydantic_js_input_core_schema=js_input_core_schema,
)
return {k: v for k, v in metadata.items() if v is not None}
pydantic_js_functions: list[GetJsonSchemaFunction]
pydantic_js_annotation_functions: list[GetJsonSchemaFunction]
pydantic_js_prefer_positional_arguments: bool
pydantic_js_input_core_schema: CoreSchema
pydantic_js_updates: JsonDict
pydantic_js_extra: JsonDict | JsonSchemaExtraCallable


def update_core_metadata(
core_metadata: Any,
/,
Comment thread
sydney-runkle marked this conversation as resolved.
*,
pydantic_js_functions: list[GetJsonSchemaFunction] | None = None,
pydantic_js_annotation_functions: list[GetJsonSchemaFunction] | None = None,
pydantic_js_updates: JsonDict | None = None,
pydantic_js_extra: JsonDict | JsonSchemaExtraCallable | None = None,
) -> None:
from ..json_schema import PydanticJsonSchemaWarning

"""Update CoreMetadata instance in place. When we make modifications in this function, they
take effect on the `core_metadata` reference passed in as the first (and only) positional argument.

First, cast to `CoreMetadata`, then finish with a cast to `dict[str, Any]` for core schema compatibility.
We do this here, instead of before / after each call to this function so that this typing hack
can be easily removed if/when we move `CoreMetadata` to `pydantic-core`.

For parameter descriptions, see `CoreMetadata` above.
"""
core_metadata = cast(CoreMetadata, core_metadata)

if pydantic_js_functions:
core_metadata.setdefault('pydantic_js_functions', []).extend(pydantic_js_functions)

if pydantic_js_annotation_functions:
core_metadata.setdefault('pydantic_js_annotation_functions', []).extend(pydantic_js_annotation_functions)

if pydantic_js_updates:
if (existing_updates := core_metadata.get('pydantic_js_updates')) is not None:
core_metadata['pydantic_js_updates'] = {**existing_updates, **pydantic_js_updates}
else:
core_metadata['pydantic_js_updates'] = pydantic_js_updates

if pydantic_js_extra is not None:
existing_pydantic_js_extra = core_metadata.get('pydantic_js_extra')
if existing_pydantic_js_extra is None:
core_metadata['pydantic_js_extra'] = pydantic_js_extra
if isinstance(existing_pydantic_js_extra, dict):
if isinstance(pydantic_js_extra, dict):
core_metadata['pydantic_js_extra'] = {**existing_pydantic_js_extra, **pydantic_js_extra}
if callable(pydantic_js_extra):
warn(
'Composing `dict` and `callable` type `json_schema_extra` is not supported.'
'The `callable` type is being ignored.'
"If you'd like support for this behavior, please open an issue on pydantic.",
PydanticJsonSchemaWarning,
)
if callable(existing_pydantic_js_extra):
# if ever there's a case of a callable, we'll just keep the last json schema extra spec
core_metadata['pydantic_js_extra'] = pydantic_js_extra
9 changes: 5 additions & 4 deletions pydantic/_internal/_core_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from typing_extensions import TypeGuard, get_args, get_origin

from . import _repr
from ._core_metadata import CoreMetadata
from ._typing_extra import TYPE_ALIAS_TYPES, is_generic_alias

AnyFunctionSchema = Union[
Expand Down Expand Up @@ -480,11 +481,11 @@ def can_be_inlined(s: core_schema.DefinitionReferenceSchema, ref: str) -> bool:
return False
if 'metadata' in s:
metadata = s['metadata']
for k in (
'pydantic_js_functions',
'pydantic_js_annotation_functions',
for k in [
*CoreMetadata.__annotations__.keys(),
'pydantic.internal.union_discriminator',
):
'pydantic.internal.tagged_union_tag',
]:
if k in metadata:
# we need to keep this as a ref
return False
Expand Down
Loading