Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pydantic/json_schema.py: 16%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""!!! abstract "Usage Documentation"
2 [JSON Schema](../concepts/json_schema.md)
4The `json_schema` module contains classes and functions to allow the way [JSON Schema](https://json-schema.org/)
5is generated to be customized.
7In general you shouldn't need to use this module directly; instead, you can use
8[`BaseModel.model_json_schema`][pydantic.BaseModel.model_json_schema] and
9[`TypeAdapter.json_schema`][pydantic.TypeAdapter.json_schema].
10"""
12from __future__ import annotations as _annotations
14import collections.abc
15import dataclasses
16import inspect
17import math
18import os
19import re
20import warnings
21from collections import Counter, defaultdict
22from collections.abc import Callable, Hashable, Iterable, Sequence
23from copy import deepcopy
24from enum import Enum
25from re import Pattern
26from typing import (
27 TYPE_CHECKING,
28 Annotated,
29 Any,
30 Literal,
31 NewType,
32 TypeAlias,
33 TypeVar,
34 cast,
35 overload,
36)
38import pydantic_core
39from pydantic_core import MISSING, CoreSchema, PydanticOmit, core_schema, to_jsonable_python
40from pydantic_core.core_schema import ComputedField
41from typing_extensions import assert_never, deprecated, final
42from typing_inspection.introspection import get_literal_values
44from pydantic.warnings import PydanticDeprecatedSince26, PydanticDeprecatedSince29
46from ._internal import (
47 _config,
48 _core_metadata,
49 _core_utils,
50 _decorators,
51 _mock_val_ser,
52 _schema_generation_shared,
53 _typing_extra,
54)
55from .annotated_handlers import GetJsonSchemaHandler
56from .config import JsonDict, JsonValue
57from .errors import PydanticInvalidForJsonSchema, PydanticUserError
59if TYPE_CHECKING:
60 from . import ConfigDict
61 from ._internal._core_utils import CoreSchemaField, CoreSchemaOrField
62 from ._internal._dataclasses import PydanticDataclass
63 from ._internal._schema_generation_shared import GetJsonSchemaFunction
64 from .main import BaseModel
67CoreSchemaOrFieldType = Literal[core_schema.CoreSchemaType, core_schema.CoreSchemaFieldType]
68"""
69A type alias for defined schema types that represents a union of
70`core_schema.CoreSchemaType` and
71`core_schema.CoreSchemaFieldType`.
72"""
74JsonSchemaValue = dict[str, Any]
75"""
76A type alias for a JSON schema value. This is a dictionary of string keys to arbitrary JSON values.
77"""
79JsonSchemaMode = Literal['validation', 'serialization']
80"""
81A type alias that represents the mode of a JSON schema; either 'validation' or 'serialization'.
83For some types, the inputs to validation differ from the outputs of serialization. For example,
84computed fields will only be present when serializing, and should not be provided when
85validating. This flag provides a way to indicate whether you want the JSON schema required
86for validation inputs, or that will be matched by serialization outputs.
87"""
89_MODE_TITLE_MAPPING: dict[JsonSchemaMode, str] = {'validation': 'Input', 'serialization': 'Output'}
92JsonSchemaWarningKind = Literal['skipped-choice', 'non-serializable-default', 'skipped-discriminator']
93"""
94A type alias representing the kinds of warnings that can be emitted during JSON schema generation.
96See [`GenerateJsonSchema.render_warning_message`][pydantic.json_schema.GenerateJsonSchema.render_warning_message]
97for more details.
98"""
101class PydanticJsonSchemaWarning(UserWarning):
102 """This class is used to emit warnings produced during JSON schema generation.
103 See the [`GenerateJsonSchema.emit_warning`][pydantic.json_schema.GenerateJsonSchema.emit_warning] and
104 [`GenerateJsonSchema.render_warning_message`][pydantic.json_schema.GenerateJsonSchema.render_warning_message]
105 methods for more details; these can be overridden to control warning behavior.
106 """
109NoDefault = object()
110"""A sentinel value used to indicate that no default value should be used when generating a JSON Schema
111for a core schema with a default value.
112"""
115# ##### JSON Schema Generation #####
116DEFAULT_REF_TEMPLATE = '#/$defs/{model}'
117"""The default format string used to generate reference names."""
119# There are three types of references relevant to building JSON schemas:
120# 1. core_schema "ref" values; these are not exposed as part of the JSON schema
121# * these might look like the fully qualified path of a model, its id, or something similar
122CoreRef = NewType('CoreRef', str)
123# 2. keys of the "definitions" object that will eventually go into the JSON schema
124# * by default, these look like "MyModel", though may change in the presence of collisions
125# * eventually, we may want to make it easier to modify the way these names are generated
126DefsRef = NewType('DefsRef', str)
127# 3. the values corresponding to the "$ref" key in the schema
128# * By default, these look like "#/$defs/MyModel", as in {"$ref": "#/$defs/MyModel"}
129JsonRef = NewType('JsonRef', str)
131CoreModeRef = tuple[CoreRef, JsonSchemaMode]
132JsonSchemaKeyT = TypeVar('JsonSchemaKeyT', bound=Hashable)
134_PRIMITIVE_JSON_SCHEMA_TYPES = ('string', 'boolean', 'null', 'integer', 'number')
137@dataclasses.dataclass(slots=True)
138class _DefinitionsRemapping:
139 defs_remapping: dict[DefsRef, DefsRef]
140 json_remapping: dict[JsonRef, JsonRef]
142 @staticmethod
143 def from_prioritized_choices(
144 prioritized_choices: dict[DefsRef, list[DefsRef]],
145 defs_to_json: dict[DefsRef, JsonRef],
146 definitions: dict[DefsRef, JsonSchemaValue],
147 ) -> _DefinitionsRemapping:
148 """
149 This function should produce a remapping that replaces complex DefsRef with the simpler ones from the
150 prioritized_choices such that applying the name remapping would result in an equivalent JSON schema.
151 """
152 # We need to iteratively simplify the definitions until we reach a fixed point.
153 # The reason for this is that outer definitions may reference inner definitions that get simplified
154 # into an equivalent reference, and the outer definitions won't be equivalent until we've simplified
155 # the inner definitions.
156 copied_definitions = deepcopy(definitions)
157 definitions_schema = {'$defs': copied_definitions}
158 for _iter in range(100): # prevent an infinite loop in the case of a bug, 100 iterations should be enough
159 # For every possible remapped DefsRef, collect all schemas that DefsRef might be used for:
160 schemas_for_alternatives: dict[DefsRef, list[JsonSchemaValue]] = defaultdict(list)
161 for defs_ref in copied_definitions:
162 alternatives = prioritized_choices[defs_ref]
163 for alternative in alternatives:
164 schemas_for_alternatives[alternative].append(copied_definitions[defs_ref])
166 # Deduplicate the schemas for each alternative; the idea is that we only want to remap to a new DefsRef
167 # if it introduces no ambiguity, i.e., there is only one distinct schema for that DefsRef.
168 for defs_ref in schemas_for_alternatives:
169 schemas_for_alternatives[defs_ref] = _deduplicate_schemas(schemas_for_alternatives[defs_ref])
171 # Build the remapping
172 defs_remapping: dict[DefsRef, DefsRef] = {}
173 json_remapping: dict[JsonRef, JsonRef] = {}
174 for original_defs_ref in definitions:
175 alternatives = prioritized_choices[original_defs_ref]
176 # Pick the first alternative that has only one schema, since that means there is no collision
177 remapped_defs_ref = next(x for x in alternatives if len(schemas_for_alternatives[x]) == 1)
178 defs_remapping[original_defs_ref] = remapped_defs_ref
180 # Map all alternatives after the remapped one to the remapped one
181 # This ensures that intermediate simplifications are also remapped
182 remapped_index = alternatives.index(remapped_defs_ref)
183 for alt in alternatives[remapped_index:]:
184 json_remapping[defs_to_json[alt]] = defs_to_json[remapped_defs_ref]
185 remapping = _DefinitionsRemapping(defs_remapping, json_remapping)
186 new_definitions_schema = remapping.remap_json_schema({'$defs': copied_definitions})
187 if definitions_schema == new_definitions_schema:
188 # We've reached the fixed point
189 return remapping
190 definitions_schema = new_definitions_schema
192 raise PydanticInvalidForJsonSchema('Failed to simplify the JSON schema definitions')
194 def remap_defs_ref(self, ref: DefsRef) -> DefsRef:
195 return self.defs_remapping.get(ref, ref)
197 def remap_json_ref(self, ref: JsonRef) -> JsonRef:
198 return self.json_remapping.get(ref, ref)
200 def remap_json_schema(self, schema: Any) -> Any:
201 """
202 Recursively update the JSON schema replacing all $refs
203 """
204 if isinstance(schema, str):
205 # Note: this may not really be a JsonRef; we rely on having no collisions between JsonRefs and other strings
206 return self.remap_json_ref(JsonRef(schema))
207 elif isinstance(schema, list):
208 return [self.remap_json_schema(item) for item in schema]
209 elif isinstance(schema, dict):
210 for key, value in schema.items():
211 if key == '$ref' and isinstance(value, str):
212 schema['$ref'] = self.remap_json_ref(JsonRef(value))
213 elif key == '$defs':
214 schema['$defs'] = {
215 self.remap_defs_ref(DefsRef(key)): self.remap_json_schema(value)
216 for key, value in schema['$defs'].items()
217 }
218 else:
219 schema[key] = self.remap_json_schema(value)
220 return schema
223class GenerateJsonSchema:
224 """!!! abstract "Usage Documentation"
225 [Customizing the JSON Schema Generation Process](../concepts/json_schema.md#customizing-the-json-schema-generation-process)
227 A class for generating JSON schemas.
229 This class generates JSON schemas based on configured parameters. The default schema dialect
230 is [https://json-schema.org/draft/2020-12/schema](https://json-schema.org/draft/2020-12/schema).
231 The class uses `by_alias` to configure how fields with
232 multiple names are handled and `ref_template` to format reference names.
234 Attributes:
235 schema_dialect: The JSON schema dialect used to generate the schema. See
236 [Declaring a Dialect](https://json-schema.org/understanding-json-schema/reference/schema.html#id4)
237 in the JSON Schema documentation for more information about dialects.
238 ignored_warning_kinds: Warnings to ignore when generating the schema. `self.render_warning_message` will
239 do nothing if its argument `kind` is in `ignored_warning_kinds`;
240 this value can be modified on subclasses to easily control which warnings are emitted.
241 by_alias: Whether to use field aliases when generating the schema.
242 ref_template: The format string used when generating reference names.
243 core_to_json_refs: A mapping of core refs to JSON refs.
244 core_to_defs_refs: A mapping of core refs to definition refs.
245 defs_to_core_refs: A mapping of definition refs to core refs.
246 json_to_defs_refs: A mapping of JSON refs to definition refs.
247 definitions: Definitions in the schema.
249 Args:
250 by_alias: Whether to use field aliases as keys in the generated JSON Schema. If `None` (the default),
251 aliases are used unless disabled by the configuration (i.e. `validate_by_alias=False` for validation
252 schemas). An explicit value takes precedence over the configuration.
253 ref_template: The format string to use when generating reference names.
254 union_format: The format to use when combining schemas from unions together. Can be one of:
256 - `'any_of'`: Use the [`anyOf`](https://json-schema.org/understanding-json-schema/reference/combining#anyOf)
257 keyword to combine schemas (the default).
258 - `'primitive_type_array'`: Use the [`type`](https://json-schema.org/understanding-json-schema/reference/type)
259 keyword as an array of strings, containing each type of the combination. If any of the schemas is not a primitive
260 type (`string`, `boolean`, `null`, `integer` or `number`) or contains constraints/metadata, falls back to
261 `any_of`.
263 Raises:
264 JsonSchemaError: If the instance of the class is inadvertently reused after generating a schema.
265 """
267 schema_dialect = 'https://json-schema.org/draft/2020-12/schema'
269 # `self.render_warning_message` will do nothing if its argument `kind` is in `ignored_warning_kinds`;
270 # this value can be modified on subclasses to easily control which warnings are emitted
271 ignored_warning_kinds: set[JsonSchemaWarningKind] = {'skipped-choice'}
273 def __init__(
274 self,
275 by_alias: bool | None = None,
276 ref_template: str = DEFAULT_REF_TEMPLATE,
277 union_format: Literal['any_of', 'primitive_type_array'] = 'any_of',
278 ) -> None:
279 # `None` means "not explicitly set": aliases are used, unless disabled by the config (e.g. `validate_by_alias`)
280 self._by_alias = by_alias
281 self.by_alias = by_alias is not False
282 self.ref_template = ref_template
283 self.union_format: Literal['any_of', 'primitive_type_array'] = union_format
285 self.core_to_json_refs: dict[CoreModeRef, JsonRef] = {}
286 self.core_to_defs_refs: dict[CoreModeRef, DefsRef] = {}
287 self.defs_to_core_refs: dict[DefsRef, CoreModeRef] = {}
288 self.json_to_defs_refs: dict[JsonRef, DefsRef] = {}
290 self.definitions: dict[DefsRef, JsonSchemaValue] = {}
291 self._config_wrapper_stack = _config.ConfigWrapperStack(_config.ConfigWrapper({}))
293 self._mode: JsonSchemaMode = 'validation'
295 # The following includes a mapping of a fully-unique defs ref choice to a list of preferred
296 # alternatives, which are generally simpler, such as only including the class name.
297 # At the end of schema generation, we use these to produce a JSON schema with more human-readable
298 # definitions, which would also work better in a generated OpenAPI client, etc.
299 self._prioritized_defsref_choices: dict[DefsRef, list[DefsRef]] = {}
300 self._collision_counter: dict[str, int] = defaultdict(int)
301 self._collision_index: dict[str, int] = {}
303 self._schema_type_to_method = self.build_schema_type_to_method()
305 # When we encounter definitions we need to try to build them immediately
306 # so that they are available schemas that reference them
307 # But it's possible that CoreSchema was never going to be used
308 # (e.g. because the CoreSchema that references short circuits is JSON schema generation without needing
309 # the reference) so instead of failing altogether if we can't build a definition we
310 # store the error raised and re-throw it if we end up needing that def
311 self._core_defs_invalid_for_json_schema: dict[DefsRef, PydanticInvalidForJsonSchema] = {}
313 # This changes to True after generating a schema, to prevent issues caused by accidental reuse
314 # of a single instance of a schema generator
315 self._used = False
317 @property
318 def _config(self) -> _config.ConfigWrapper:
319 return self._config_wrapper_stack.tail
321 @property
322 def mode(self) -> JsonSchemaMode:
323 if self._config.json_schema_mode_override is not None:
324 return self._config.json_schema_mode_override
325 else:
326 return self._mode
328 def build_schema_type_to_method(
329 self,
330 ) -> dict[CoreSchemaOrFieldType, Callable[[CoreSchemaOrField], JsonSchemaValue]]:
331 """Builds a dictionary mapping fields to methods for generating JSON schemas.
333 Returns:
334 A dictionary containing the mapping of `CoreSchemaOrFieldType` to a handler method.
336 Raises:
337 TypeError: If no method has been defined for generating a JSON schema for a given pydantic core schema type.
338 """
339 mapping: dict[CoreSchemaOrFieldType, Callable[[CoreSchemaOrField], JsonSchemaValue]] = {}
340 core_schema_types: list[CoreSchemaOrFieldType] = list(get_literal_values(CoreSchemaOrFieldType))
341 for key in core_schema_types:
342 method_name = f'{key.replace("-", "_")}_schema'
343 try:
344 mapping[key] = getattr(self, method_name)
345 except AttributeError as e: # pragma: no cover
346 if os.getenv('PYDANTIC_PRIVATE_ALLOW_UNHANDLED_SCHEMA_TYPES'):
347 continue
348 raise TypeError(
349 f'No method for generating JsonSchema for core_schema.type={key!r} '
350 f'(expected: {type(self).__name__}.{method_name})'
351 ) from e
352 return mapping
354 def generate_definitions(
355 self, inputs: Sequence[tuple[JsonSchemaKeyT, JsonSchemaMode, core_schema.CoreSchema]]
356 ) -> tuple[dict[tuple[JsonSchemaKeyT, JsonSchemaMode], JsonSchemaValue], dict[DefsRef, JsonSchemaValue]]:
357 """Generates JSON schema definitions from a list of core schemas, pairing the generated definitions with a
358 mapping that links the input keys to the definition references.
360 Args:
361 inputs: A sequence of tuples, where:
363 - The first element is a JSON schema key type.
364 - The second element is the JSON mode: either 'validation' or 'serialization'.
365 - The third element is a core schema.
367 Returns:
368 A tuple where:
370 - The first element is a dictionary whose keys are tuples of JSON schema key type and JSON mode, and
371 whose values are the JSON schema corresponding to that pair of inputs. (These schemas may have
372 JsonRef references to definitions that are defined in the second returned element.)
373 - The second element is a dictionary whose keys are definition references for the JSON schemas
374 from the first returned element, and whose values are the actual JSON schema definitions.
376 Raises:
377 PydanticUserError: Raised if the JSON schema generator has already been used to generate a JSON schema.
378 """
379 if self._used:
380 raise PydanticUserError(
381 'This JSON schema generator has already been used to generate a JSON schema. '
382 f'You must create a new instance of {type(self).__name__} to generate a new JSON schema.',
383 code='json-schema-already-used',
384 )
386 for _, mode, schema in inputs:
387 self._mode = mode
388 self.generate_inner(schema)
390 definitions_remapping = self._build_definitions_remapping()
392 json_schemas_map: dict[tuple[JsonSchemaKeyT, JsonSchemaMode], DefsRef] = {}
393 for key, mode, schema in inputs:
394 self._mode = mode
395 json_schema = self.generate_inner(schema)
396 json_schemas_map[(key, mode)] = definitions_remapping.remap_json_schema(json_schema)
398 json_schema = {'$defs': self.definitions}
399 json_schema = definitions_remapping.remap_json_schema(json_schema)
400 self._used = True
401 return json_schemas_map, self.sort(json_schema['$defs']) # type: ignore
403 def generate(self, schema: CoreSchema, mode: JsonSchemaMode = 'validation') -> JsonSchemaValue:
404 """Generates a JSON schema for a specified schema in a specified mode.
406 Args:
407 schema: A Pydantic model.
408 mode: The mode in which to generate the schema. Defaults to 'validation'.
410 Returns:
411 A JSON schema representing the specified schema.
413 Raises:
414 PydanticUserError: If the JSON schema generator has already been used to generate a JSON schema.
415 """
416 self._mode = mode
417 if self._used:
418 raise PydanticUserError(
419 'This JSON schema generator has already been used to generate a JSON schema. '
420 f'You must create a new instance of {type(self).__name__} to generate a new JSON schema.',
421 code='json-schema-already-used',
422 )
424 json_schema: JsonSchemaValue = self.generate_inner(schema)
425 json_ref_counts = self.get_json_ref_counts(json_schema)
427 ref = cast(JsonRef, json_schema.get('$ref'))
428 while ref is not None: # may need to unpack multiple levels
429 ref_json_schema = self.get_schema_from_definitions(ref)
430 if json_ref_counts[ref] == 1 and ref_json_schema is not None and len(json_schema) == 1:
431 # "Unpack" the ref since this is the only reference and there are no sibling keys
432 json_schema = ref_json_schema.copy() # copy to prevent recursive dict reference
433 json_ref_counts[ref] -= 1
434 ref = cast(JsonRef, json_schema.get('$ref'))
435 ref = None
437 self._garbage_collect_definitions(json_schema)
438 definitions_remapping = self._build_definitions_remapping()
440 if self.definitions:
441 json_schema['$defs'] = self.definitions
443 json_schema = definitions_remapping.remap_json_schema(json_schema)
445 # For now, we will not set the $schema key. However, if desired, this can be easily added by overriding
446 # this method and adding the following line after a call to super().generate(schema):
447 # json_schema['$schema'] = self.schema_dialect
449 self._used = True
450 return self.sort(json_schema)
452 def generate_inner(self, schema: CoreSchemaOrField) -> JsonSchemaValue: # noqa: C901
453 """Generates a JSON schema for a given core schema.
455 Args:
456 schema: The given core schema.
458 Returns:
459 The generated JSON schema.
461 TODO: the nested function definitions here seem like bad practice, I'd like to unpack these
462 in a future PR. It'd be great if we could shorten the call stack a bit for JSON schema generation,
463 and I think there's potential for that here.
464 """
465 # If a schema with the same CoreRef has been handled, just return a reference to it
466 # Note that this assumes that it will _never_ be the case that the same CoreRef is used
467 # on types that should have different JSON schemas
468 if 'ref' in schema:
469 core_ref = CoreRef(schema['ref']) # type: ignore[typeddict-item]
470 core_mode_ref = (core_ref, self.mode)
471 if core_mode_ref in self.core_to_defs_refs and self.core_to_defs_refs[core_mode_ref] in self.definitions:
472 return {'$ref': self.core_to_json_refs[core_mode_ref]}
474 def populate_defs(core_schema: CoreSchema, json_schema: JsonSchemaValue) -> JsonSchemaValue:
475 if 'ref' in core_schema:
476 core_ref = CoreRef(core_schema['ref']) # type: ignore[typeddict-item]
477 defs_ref, ref_json_schema = self.get_cache_defs_ref_schema(core_ref)
478 json_ref = JsonRef(ref_json_schema['$ref'])
479 # Replace the schema if it's not a reference to itself
480 # What we want to avoid is having the def be just a ref to itself
481 # which is what would happen if we blindly assigned any
482 if json_schema.get('$ref', None) != json_ref:
483 self.definitions[defs_ref] = json_schema
484 self._core_defs_invalid_for_json_schema.pop(defs_ref, None)
485 json_schema = ref_json_schema
486 return json_schema
488 def handler_func(schema_or_field: CoreSchemaOrField) -> JsonSchemaValue:
489 """Generate a JSON schema based on the input schema.
491 Args:
492 schema_or_field: The core schema to generate a JSON schema from.
494 Returns:
495 The generated JSON schema.
497 Raises:
498 TypeError: If an unexpected schema type is encountered.
499 """
500 # Generate the core-schema-type-specific bits of the schema generation:
501 json_schema: JsonSchemaValue | None = None
502 if self.mode == 'serialization' and 'serialization' in schema_or_field:
503 # In this case, we skip the JSON Schema generation of the schema
504 # and use the `'serialization'` schema instead (canonical example:
505 # `Annotated[int, PlainSerializer(str)]`).
506 ser_schema = schema_or_field['serialization'] # type: ignore
507 json_schema = self.ser_schema(ser_schema)
509 # It might be that the 'serialization'` is skipped depending on `when_used`.
510 # This is only relevant for `nullable` schemas though (possibly wrapped, e.g. when
511 # using a field serializer on a field with a default value), so we special case here.
512 if (
513 json_schema is not None
514 and ser_schema.get('when_used') in ('unless-none', 'json-unless-none')
515 and _is_nullable_schema(schema_or_field)
516 ):
517 json_schema = self.get_union_of_schemas([{'type': 'null'}, json_schema])
518 if json_schema is None:
519 if _core_utils.is_core_schema(schema_or_field) or _core_utils.is_core_schema_field(schema_or_field):
520 generate_for_schema_type = self._schema_type_to_method[schema_or_field['type']]
521 json_schema = generate_for_schema_type(schema_or_field)
522 else:
523 raise TypeError(f'Unexpected schema type: schema={schema_or_field}')
524 return json_schema
526 current_handler = _schema_generation_shared.GenerateJsonSchemaHandler(self, handler_func)
528 metadata = cast(_core_metadata.CoreMetadata, schema.get('metadata', {}))
530 # TODO: I dislike that we have to wrap these basic dict updates in callables, is there any way around this?
532 if js_updates := metadata.get('pydantic_js_updates'):
534 def js_updates_handler_func(
535 schema_or_field: CoreSchemaOrField,
536 current_handler: GetJsonSchemaHandler = current_handler,
537 ) -> JsonSchemaValue:
538 json_schema = {**current_handler(schema_or_field), **js_updates}
539 return json_schema
541 current_handler = _schema_generation_shared.GenerateJsonSchemaHandler(self, js_updates_handler_func)
543 if js_extra := metadata.get('pydantic_js_extra'):
545 def js_extra_handler_func(
546 schema_or_field: CoreSchemaOrField,
547 current_handler: GetJsonSchemaHandler = current_handler,
548 ) -> JsonSchemaValue:
549 json_schema = current_handler(schema_or_field)
550 if isinstance(js_extra, dict):
551 json_schema.update(to_jsonable_python(js_extra))
552 elif callable(js_extra):
553 # similar to typing issue in _update_class_schema when we're working with callable js extra
554 js_extra(json_schema) # type: ignore
555 return json_schema
557 current_handler = _schema_generation_shared.GenerateJsonSchemaHandler(self, js_extra_handler_func)
559 for js_modify_function in metadata.get('pydantic_js_functions', ()):
561 def new_handler_func(
562 schema_or_field: CoreSchemaOrField,
563 current_handler: GetJsonSchemaHandler = current_handler,
564 js_modify_function: GetJsonSchemaFunction = js_modify_function,
565 ) -> JsonSchemaValue:
566 json_schema = js_modify_function(schema_or_field, current_handler)
567 if _core_utils.is_core_schema(schema_or_field):
568 json_schema = populate_defs(schema_or_field, json_schema)
569 original_schema = current_handler.resolve_ref_schema(json_schema)
570 ref = json_schema.pop('$ref', None)
571 if ref and json_schema:
572 original_schema.update(json_schema)
573 return original_schema
575 current_handler = _schema_generation_shared.GenerateJsonSchemaHandler(self, new_handler_func)
577 for js_modify_function in metadata.get('pydantic_js_annotation_functions', ()):
579 def new_handler_func(
580 schema_or_field: CoreSchemaOrField,
581 current_handler: GetJsonSchemaHandler = current_handler,
582 js_modify_function: GetJsonSchemaFunction = js_modify_function,
583 ) -> JsonSchemaValue:
584 return js_modify_function(schema_or_field, current_handler)
586 current_handler = _schema_generation_shared.GenerateJsonSchemaHandler(self, new_handler_func)
588 json_schema = current_handler(schema)
589 if _core_utils.is_core_schema(schema):
590 json_schema = populate_defs(schema, json_schema)
591 return json_schema
593 def sort(self, value: JsonSchemaValue, parent_key: str | None = None) -> JsonSchemaValue:
594 """Override this method to customize the sorting of the JSON schema (e.g., don't sort at all, sort all keys unconditionally, etc.)
596 By default, alphabetically sort the keys in the JSON schema, skipping the 'properties' and 'default' keys to preserve field definition order.
597 This sort is recursive, so it will sort all nested dictionaries as well.
598 """
599 sorted_dict: dict[str, JsonSchemaValue] = {}
600 keys = value.keys()
601 if parent_key not in ('properties', 'default'):
602 keys = sorted(keys)
603 for key in keys:
604 sorted_dict[key] = self._sort_recursive(value[key], parent_key=key)
605 return sorted_dict
607 def _sort_recursive(self, value: Any, parent_key: str | None = None) -> Any:
608 """Recursively sort a JSON schema value."""
609 if isinstance(value, dict):
610 sorted_dict: dict[str, JsonSchemaValue] = {}
611 keys = value.keys()
612 if parent_key not in ('properties', 'default'):
613 keys = sorted(keys)
614 for key in keys:
615 sorted_dict[key] = self._sort_recursive(value[key], parent_key=key)
616 return sorted_dict
617 elif isinstance(value, list):
618 sorted_list: list[JsonSchemaValue] = [self._sort_recursive(item, parent_key) for item in value]
619 return sorted_list
620 else:
621 return value
623 # ### Schema generation methods
625 def invalid_schema(self, schema: core_schema.InvalidSchema) -> JsonSchemaValue:
626 """Placeholder - should never be called."""
628 raise RuntimeError('Cannot generate schema for invalid_schema. This is a bug! Please report it.')
630 def any_schema(self, schema: core_schema.AnySchema) -> JsonSchemaValue:
631 """Generates a JSON schema that matches any value.
633 Args:
634 schema: The core schema.
636 Returns:
637 The generated JSON schema.
638 """
639 return {}
641 def none_schema(self, schema: core_schema.NoneSchema) -> JsonSchemaValue:
642 """Generates a JSON schema that matches `None`.
644 Args:
645 schema: The core schema.
647 Returns:
648 The generated JSON schema.
649 """
650 return {'type': 'null'}
652 def bool_schema(self, schema: core_schema.BoolSchema) -> JsonSchemaValue:
653 """Generates a JSON schema that matches a bool value.
655 Args:
656 schema: The core schema.
658 Returns:
659 The generated JSON schema.
660 """
661 return {'type': 'boolean'}
663 def int_schema(self, schema: core_schema.IntSchema) -> JsonSchemaValue:
664 """Generates a JSON schema that matches an int value.
666 Args:
667 schema: The core schema.
669 Returns:
670 The generated JSON schema.
671 """
672 json_schema: dict[str, Any] = {'type': 'integer'}
673 self.update_with_validations(json_schema, schema, self.ValidationsMapping.numeric)
674 json_schema = {k: v for k, v in json_schema.items() if v not in {math.inf, -math.inf}}
675 return json_schema
677 def float_schema(self, schema: core_schema.FloatSchema) -> JsonSchemaValue:
678 """Generates a JSON schema that matches a float value.
680 Args:
681 schema: The core schema.
683 Returns:
684 The generated JSON schema.
685 """
686 json_schema: dict[str, Any] = {'type': 'number'}
687 self.update_with_validations(json_schema, schema, self.ValidationsMapping.numeric)
688 json_schema = {k: v for k, v in json_schema.items() if v not in {math.inf, -math.inf}}
689 return json_schema
691 def fraction_schema(self, schema: core_schema.FractionSchema) -> JsonSchemaValue:
692 """Generates a JSON schema that matches a fraction value.
694 Args:
695 schema: The core schema.
697 Returns:
698 The generated JSON schema.
700 """
701 json_schema: JsonSchemaValue = {'type': 'string', 'format': 'fraction'}
702 if self.mode == 'validation':
703 le = schema.get('le')
704 ge = schema.get('ge')
705 lt = schema.get('lt')
706 gt = schema.get('gt')
707 json_schema = {
708 'anyOf': [
709 self.float_schema(
710 core_schema.float_schema(
711 le=None if le is None else float(le),
712 ge=None if ge is None else float(ge),
713 lt=None if lt is None else float(lt),
714 gt=None if gt is None else float(gt),
715 )
716 ),
717 json_schema,
718 ],
719 }
720 return json_schema
722 def decimal_schema(self, schema: core_schema.DecimalSchema) -> JsonSchemaValue:
723 """Generates a JSON schema that matches a decimal value.
725 Args:
726 schema: The core schema.
728 Returns:
729 The generated JSON schema.
730 """
731 str_schema = core_schema.str_schema()
732 pattern = self.get_decimal_pattern(schema)
733 if pattern is not None:
734 str_schema['pattern'] = pattern
735 json_schema = self.str_schema(str_schema)
736 if self.mode == 'validation':
737 multiple_of = schema.get('multiple_of')
738 le = schema.get('le')
739 ge = schema.get('ge')
740 lt = schema.get('lt')
741 gt = schema.get('gt')
742 json_schema = {
743 'anyOf': [
744 self.float_schema(
745 core_schema.float_schema(
746 allow_inf_nan=schema.get('allow_inf_nan'),
747 multiple_of=None if multiple_of is None else float(multiple_of),
748 le=None if le is None else float(le),
749 ge=None if ge is None else float(ge),
750 lt=None if lt is None else float(lt),
751 gt=None if gt is None else float(gt),
752 )
753 ),
754 json_schema,
755 ],
756 }
757 return json_schema
759 def get_decimal_pattern(self, schema: core_schema.DecimalSchema) -> str | None:
760 """Get the regular expression pattern to apply to the string representation of a decimal.
762 By default, no pattern is applied (`None` is returned). Subclasses can override this method
763 to return a custom pattern, or [`build_decimal_pattern()`][pydantic.json_schema.GenerateJsonSchema.build_decimal_pattern]
764 can be used to get a pattern reflecting the `max_digits` and `decimal_places` constraints:
766 ```python
767 from pydantic.json_schema import GenerateJsonSchema
769 class MyGenerateJsonSchema(GenerateJsonSchema):
770 def get_decimal_pattern(self, schema):
771 return self.build_decimal_pattern(schema)
772 ```
774 Args:
775 schema: The core schema.
777 Returns:
778 The regular expression pattern, or `None` if no pattern should be applied.
779 """
780 return None
782 def build_decimal_pattern(self, schema: core_schema.DecimalSchema) -> str:
783 """Build a regular expression pattern for the string representation of a decimal.
785 The pattern takes into account the `max_digits` and `decimal_places` constraints, and the current
786 mode:
788 - In `'validation'` mode, the pattern accepts the string syntax understood by the [`Decimal`][decimal.Decimal]
789 constructor. The `max_digits` and `decimal_places` constraints are only reflected for strings
790 *without* an exponent: strings using an exponent are accepted as long as they are syntactically valid,
791 as the constraints can't be expressed with a regular expression in this case.
792 - In `'serialization'` mode, the pattern exactly describes the strings produced by pydantic
793 (i.e. by `str(decimal)`) for decimals satisfying the constraints,
794 including the ones using scientific notation (e.g. `'1E-7'`, `'1.5E+3'`).
796 In both modes, trailing zeros in the decimal part are not counted (e.g. `'1.10'` is accepted with
797 `decimal_places=1`), consistent with the validation behavior. If `allow_inf_nan` is enabled (on the
798 core schema or in the configuration), the string representations of infinity and NaN are also accepted.
800 Args:
801 schema: The core schema.
803 Returns:
804 The regular expression pattern.
805 """
806 max_digits = schema.get('max_digits')
807 decimal_places = schema.get('decimal_places')
808 allow_inf_nan = schema.get('allow_inf_nan')
809 if allow_inf_nan is None:
810 allow_inf_nan = self._config.config_dict.get('allow_inf_nan', False)
811 if self.mode == 'validation':
812 return _decimal_validation_pattern(max_digits, decimal_places, allow_inf_nan)
813 else:
814 return _decimal_serialization_pattern(max_digits, decimal_places, allow_inf_nan)
816 def str_schema(self, schema: core_schema.StringSchema) -> JsonSchemaValue:
817 """Generates a JSON schema that matches a string value.
819 Args:
820 schema: The core schema.
822 Returns:
823 The generated JSON schema.
824 """
825 json_schema: JsonSchemaValue = {'type': 'string'}
826 self.update_with_validations(json_schema, schema, self.ValidationsMapping.string)
827 if 'minLength' not in json_schema and self._config.str_min_length:
828 json_schema['minLength'] = self._config.str_min_length
829 if 'maxLength' not in json_schema and self._config.str_max_length is not None:
830 json_schema['maxLength'] = self._config.str_max_length
831 if isinstance(json_schema.get('pattern'), Pattern):
832 # TODO: should we add regex flags to the pattern?
833 json_schema['pattern'] = json_schema.get('pattern').pattern # type: ignore
834 return json_schema
836 def bytes_schema(self, schema: core_schema.BytesSchema) -> JsonSchemaValue:
837 """Generates a JSON schema that matches a bytes value.
839 Args:
840 schema: The core schema.
842 Returns:
843 The generated JSON schema.
844 """
845 json_schema = {'type': 'string', 'format': 'base64url' if self._config.ser_json_bytes == 'base64' else 'binary'}
846 self.update_with_validations(json_schema, schema, self.ValidationsMapping.bytes)
847 return json_schema
849 def date_schema(self, schema: core_schema.DateSchema) -> JsonSchemaValue:
850 """Generates a JSON schema that matches a date value.
852 Args:
853 schema: The core schema.
855 Returns:
856 The generated JSON schema.
857 """
858 return self._common_temporal_schema('date', self._config.ser_json_temporal)
860 def time_schema(self, schema: core_schema.TimeSchema) -> JsonSchemaValue:
861 """Generates a JSON schema that matches a time value.
863 Args:
864 schema: The core schema.
866 Returns:
867 The generated JSON schema.
868 """
869 return self._common_temporal_schema('time', self._config.ser_json_temporal)
871 def datetime_schema(self, schema: core_schema.DatetimeSchema) -> JsonSchemaValue:
872 """Generates a JSON schema that matches a datetime value.
874 Args:
875 schema: The core schema.
877 Returns:
878 The generated JSON schema.
879 """
880 return self._common_temporal_schema('date-time', self._config.ser_json_temporal)
882 def timedelta_schema(self, schema: core_schema.TimedeltaSchema) -> JsonSchemaValue:
883 """Generates a JSON schema that matches a timedelta value.
885 Args:
886 schema: The core schema.
888 Returns:
889 The generated JSON schema.
890 """
891 if 'ser_json_temporal' in self._config.config_dict:
892 temporal_format = self._config.ser_json_temporal
893 else:
894 # `ser_json_temporal` supersedes `ser_json_timedelta`, which only applies when the former isn't
895 # explicitly set.
896 temporal_format = 'seconds' if self._config.ser_json_timedelta == 'float' else 'iso8601'
897 return self._common_temporal_schema('duration', temporal_format)
899 def _common_temporal_schema(
900 self, format: str, temporal_format: Literal['iso8601', 'seconds', 'milliseconds']
901 ) -> JsonSchemaValue:
902 if self.mode == 'serialization' and temporal_format != 'iso8601':
903 # Both `'seconds'` and `'milliseconds'` serialize to a number:
904 return {'type': 'number'}
905 return {'type': 'string', 'format': format}
907 def literal_schema(self, schema: core_schema.LiteralSchema) -> JsonSchemaValue:
908 """Generates a JSON schema that matches a literal value.
910 Args:
911 schema: The core schema.
913 Returns:
914 The generated JSON schema.
915 """
916 expected = [to_jsonable_python(v.value if isinstance(v, Enum) else v) for v in schema['expected']]
918 result: dict[str, Any] = {}
919 if len(expected) == 1:
920 result['const'] = expected[0]
921 else:
922 result['enum'] = expected
924 types = {type(e) for e in expected}
925 if types == {str}:
926 result['type'] = 'string'
927 elif types == {int}:
928 result['type'] = 'integer'
929 elif types == {float}:
930 result['type'] = 'number'
931 elif types == {bool}:
932 result['type'] = 'boolean'
933 elif types == {list}:
934 result['type'] = 'array'
935 elif types == {type(None)}:
936 result['type'] = 'null'
937 return result
939 def missing_sentinel_schema(self, schema: core_schema.MissingSentinelSchema) -> JsonSchemaValue:
940 """Generates a JSON schema that matches a schema that allows the `MISSING` sentinel value.
942 Args:
943 schema: The core schema.
945 Returns:
946 The generated JSON schema.
947 """
948 inner_schema = schema.get('schema')
949 if inner_schema is None:
950 raise PydanticOmit
951 return self.generate_inner(inner_schema)
953 def ellipsis_schema(self, schema: core_schema.EllipsisSchema) -> JsonSchemaValue:
954 """Handles JSON schema generation for a core schema that checks if a value is the [`Ellipsis`][] literal.
956 Unless overridden in a subclass, this raises an error.
958 Args:
959 schema: The core schema.
961 Returns:
962 The generated JSON schema.
963 """
964 return self.handle_invalid_for_json_schema(schema, 'core_schema.EllipsisSchema')
966 def enum_schema(self, schema: core_schema.EnumSchema) -> JsonSchemaValue:
967 """Generates a JSON schema that matches an Enum value.
969 Args:
970 schema: The core schema.
972 Returns:
973 The generated JSON schema.
974 """
975 enum_type = schema['cls']
976 description = None if not enum_type.__doc__ else inspect.cleandoc(enum_type.__doc__)
977 if (
978 description == 'An enumeration.'
979 ): # This is the default value provided by enum.EnumMeta.__new__; don't use it
980 description = None
981 result: dict[str, Any] = {'title': enum_type.__name__, 'description': description}
982 result = {k: v for k, v in result.items() if v is not None}
984 expected = [to_jsonable_python(v.value) for v in schema['members']]
986 result['enum'] = expected
988 types = {type(e) for e in expected}
989 if isinstance(enum_type, str) or types == {str}:
990 result['type'] = 'string'
991 elif isinstance(enum_type, int) or types == {int}:
992 result['type'] = 'integer'
993 elif isinstance(enum_type, float) or types == {float}:
994 result['type'] = 'number'
995 elif types == {bool}:
996 result['type'] = 'boolean'
997 elif types == {list}:
998 result['type'] = 'array'
1000 return result
1002 def is_instance_schema(self, schema: core_schema.IsInstanceSchema) -> JsonSchemaValue:
1003 """Handles JSON schema generation for a core schema that checks if a value is an instance of a class.
1005 Unless overridden in a subclass, this raises an error.
1007 Args:
1008 schema: The core schema.
1010 Returns:
1011 The generated JSON schema.
1012 """
1013 return self.handle_invalid_for_json_schema(schema, f'core_schema.IsInstanceSchema ({schema["cls"]})')
1015 def is_subclass_schema(self, schema: core_schema.IsSubclassSchema) -> JsonSchemaValue:
1016 """Handles JSON schema generation for a core schema that checks if a value is a subclass of a class.
1018 For backwards compatibility with v1, this does not raise an error, but can be overridden to change this.
1020 Args:
1021 schema: The core schema.
1023 Returns:
1024 The generated JSON schema.
1025 """
1026 # Note: This is for compatibility with V1; you can override if you want different behavior.
1027 return {}
1029 def callable_schema(self, schema: core_schema.CallableSchema) -> JsonSchemaValue:
1030 """Generates a JSON schema that matches a callable value.
1032 Unless overridden in a subclass, this raises an error.
1034 Args:
1035 schema: The core schema.
1037 Returns:
1038 The generated JSON schema.
1039 """
1040 return self.handle_invalid_for_json_schema(schema, 'core_schema.CallableSchema')
1042 def list_schema(self, schema: core_schema.ListSchema) -> JsonSchemaValue:
1043 """Returns a schema that matches a list schema.
1045 Args:
1046 schema: The core schema.
1048 Returns:
1049 The generated JSON schema.
1050 """
1051 items_schema = {} if 'items_schema' not in schema else self.generate_inner(schema['items_schema'])
1052 json_schema = {'type': 'array', 'items': items_schema}
1053 self.update_with_validations(json_schema, schema, self.ValidationsMapping.array)
1054 return json_schema
1056 def deque_schema(self, schema: core_schema.DequeSchema) -> JsonSchemaValue:
1057 """Returns a schema that matches a deque schema.
1059 Args:
1060 schema: The core schema.
1062 Returns:
1063 The generated JSON schema.
1064 """
1065 items_schema = {} if 'items_schema' not in schema else self.generate_inner(schema['items_schema'])
1066 json_schema = {'type': 'array', 'items': items_schema}
1067 self.update_with_validations(json_schema, schema, self.ValidationsMapping.array)
1068 return json_schema
1070 @deprecated('`tuple_positional_schema` is deprecated. Use `tuple_schema` instead.', category=None)
1071 @final
1072 def tuple_positional_schema(self, schema: core_schema.TupleSchema) -> JsonSchemaValue:
1073 """Replaced by `tuple_schema`."""
1074 warnings.warn(
1075 '`tuple_positional_schema` is deprecated. Use `tuple_schema` instead.',
1076 PydanticDeprecatedSince26,
1077 stacklevel=2,
1078 )
1079 return self.tuple_schema(schema)
1081 @deprecated('`tuple_variable_schema` is deprecated. Use `tuple_schema` instead.', category=None)
1082 @final
1083 def tuple_variable_schema(self, schema: core_schema.TupleSchema) -> JsonSchemaValue:
1084 """Replaced by `tuple_schema`."""
1085 warnings.warn(
1086 '`tuple_variable_schema` is deprecated. Use `tuple_schema` instead.',
1087 PydanticDeprecatedSince26,
1088 stacklevel=2,
1089 )
1090 return self.tuple_schema(schema)
1092 def tuple_schema(self, schema: core_schema.TupleSchema) -> JsonSchemaValue:
1093 """Generates a JSON schema that matches a tuple schema e.g. `tuple[int,
1094 str, bool]` or `tuple[int, ...]`.
1096 Args:
1097 schema: The core schema.
1099 Returns:
1100 The generated JSON schema.
1101 """
1102 json_schema: JsonSchemaValue = {'type': 'array'}
1103 if 'variadic_item_index' in schema:
1104 variadic_item_index = schema['variadic_item_index']
1105 if variadic_item_index > 0:
1106 json_schema['minItems'] = variadic_item_index
1107 json_schema['prefixItems'] = [
1108 self.generate_inner(item) for item in schema['items_schema'][:variadic_item_index]
1109 ]
1110 if variadic_item_index + 1 == len(schema['items_schema']):
1111 # if the variadic item is the last item, then represent it faithfully
1112 json_schema['items'] = self.generate_inner(schema['items_schema'][variadic_item_index])
1113 else:
1114 # otherwise, 'items' represents the schema for the variadic
1115 # item plus the suffix, so just allow anything for simplicity
1116 # for now
1117 json_schema['items'] = True
1118 else:
1119 prefixItems = [self.generate_inner(item) for item in schema['items_schema']]
1120 if prefixItems:
1121 json_schema['prefixItems'] = prefixItems
1122 json_schema['minItems'] = len(prefixItems)
1123 json_schema['maxItems'] = len(prefixItems)
1124 self.update_with_validations(json_schema, schema, self.ValidationsMapping.array)
1125 return json_schema
1127 def set_schema(self, schema: core_schema.SetSchema) -> JsonSchemaValue:
1128 """Generates a JSON schema that matches a set schema.
1130 Args:
1131 schema: The core schema.
1133 Returns:
1134 The generated JSON schema.
1135 """
1136 return self._common_set_schema(schema)
1138 def frozenset_schema(self, schema: core_schema.FrozenSetSchema) -> JsonSchemaValue:
1139 """Generates a JSON schema that matches a frozenset schema.
1141 Args:
1142 schema: The core schema.
1144 Returns:
1145 The generated JSON schema.
1146 """
1147 return self._common_set_schema(schema)
1149 def _common_set_schema(self, schema: core_schema.SetSchema | core_schema.FrozenSetSchema) -> JsonSchemaValue:
1150 items_schema = {} if 'items_schema' not in schema else self.generate_inner(schema['items_schema'])
1151 json_schema = {'type': 'array', 'uniqueItems': True, 'items': items_schema}
1152 self.update_with_validations(json_schema, schema, self.ValidationsMapping.array)
1153 return json_schema
1155 def generator_schema(self, schema: core_schema.GeneratorSchema) -> JsonSchemaValue:
1156 """Returns a JSON schema that represents the provided GeneratorSchema.
1158 Args:
1159 schema: The schema.
1161 Returns:
1162 The generated JSON schema.
1163 """
1164 items_schema = {} if 'items_schema' not in schema else self.generate_inner(schema['items_schema'])
1165 json_schema = {'type': 'array', 'items': items_schema}
1166 self.update_with_validations(json_schema, schema, self.ValidationsMapping.array)
1167 return json_schema
1169 def dict_schema(self, schema: core_schema.DictSchema) -> JsonSchemaValue:
1170 """Generates a JSON schema that matches a dict schema.
1172 Args:
1173 schema: The core schema.
1175 Returns:
1176 The generated JSON schema.
1177 """
1178 return self._common_dict_schema(schema)
1180 def frozendict_schema(self, schema: core_schema.FrozenDictSchema) -> JsonSchemaValue:
1181 """Generates a JSON schema that matches a frozendict schema.
1183 Args:
1184 schema: The core schema.
1186 Returns:
1187 The generated JSON schema.
1188 """
1189 return self._common_dict_schema(schema)
1191 def ordered_dict_schema(self, schema: core_schema.OrderedDictSchema) -> JsonSchemaValue:
1192 """Generates a JSON schema that matches an `OrderedDict` schema.
1194 Args:
1195 schema: The core schema.
1197 Returns:
1198 The generated JSON schema.
1199 """
1200 return self._common_dict_schema(schema)
1202 def counter_schema(self, schema: core_schema.CounterSchema) -> JsonSchemaValue:
1203 """Generates a JSON schema that matches a `Counter` schema.
1205 Args:
1206 schema: The core schema.
1208 Returns:
1209 The generated JSON schema.
1210 """
1211 return self._common_dict_schema(schema)
1213 def _common_dict_schema(
1214 self,
1215 schema: core_schema.DictSchema
1216 | core_schema.FrozenDictSchema
1217 | core_schema.OrderedDictSchema
1218 | core_schema.CounterSchema,
1219 ) -> JsonSchemaValue:
1220 json_schema: JsonSchemaValue = {'type': 'object'}
1222 keys_schema = self.generate_inner(schema['keys_schema']).copy() if 'keys_schema' in schema else {}
1223 if '$ref' not in keys_schema:
1224 keys_pattern = keys_schema.pop('pattern', None)
1225 # Don't give a title to patternProperties/propertyNames:
1226 keys_schema.pop('title', None)
1227 else:
1228 # Here, we assume that if the keys schema is a definition reference,
1229 # it can't be a simple string core schema (and thus no pattern can exist).
1230 # However, this is only in practice (in theory, a definition reference core
1231 # schema could be generated for a simple string schema).
1232 # Note that we avoid calling `self.resolve_ref_schema`, as it might not exist yet.
1233 keys_pattern = None
1235 values_schema = self.generate_inner(schema['values_schema']).copy() if 'values_schema' in schema else {}
1236 # don't give a title to additionalProperties:
1237 values_schema.pop('title', None)
1239 if values_schema or keys_pattern is not None:
1240 if keys_pattern is None:
1241 json_schema['additionalProperties'] = values_schema
1242 else:
1243 json_schema['patternProperties'] = {keys_pattern: values_schema}
1244 else: # for `dict[str, Any]`, we allow any key and any value, since `str` is the default key type
1245 json_schema['additionalProperties'] = True
1247 if (
1248 # The len check indicates that constraints are probably present:
1249 (keys_schema.get('type') == 'string' and len(keys_schema) > 1)
1250 # If this is a definition reference schema, it most likely has constraints:
1251 or '$ref' in keys_schema
1252 ):
1253 keys_schema.pop('type', None)
1254 json_schema['propertyNames'] = keys_schema
1256 self.update_with_validations(json_schema, schema, self.ValidationsMapping.object)
1257 return json_schema
1259 def function_before_schema(self, schema: core_schema.BeforeValidatorFunctionSchema) -> JsonSchemaValue:
1260 """Generates a JSON schema that matches a function-before schema.
1262 Args:
1263 schema: The core schema.
1265 Returns:
1266 The generated JSON schema.
1267 """
1268 if self.mode == 'validation' and (input_schema := schema.get('json_schema_input_schema')):
1269 return self.generate_inner(input_schema)
1271 return self.generate_inner(schema['schema'])
1273 def function_after_schema(self, schema: core_schema.AfterValidatorFunctionSchema) -> JsonSchemaValue:
1274 """Generates a JSON schema that matches a function-after schema.
1276 Args:
1277 schema: The core schema.
1279 Returns:
1280 The generated JSON schema.
1281 """
1282 return self.generate_inner(schema['schema'])
1284 def function_plain_schema(self, schema: core_schema.PlainValidatorFunctionSchema) -> JsonSchemaValue:
1285 """Generates a JSON schema that matches a function-plain schema.
1287 Args:
1288 schema: The core schema.
1290 Returns:
1291 The generated JSON schema.
1292 """
1293 if self.mode == 'validation' and (input_schema := schema.get('json_schema_input_schema')):
1294 return self.generate_inner(input_schema)
1296 return self.handle_invalid_for_json_schema(
1297 schema, f'core_schema.PlainValidatorFunctionSchema ({schema["function"]})'
1298 )
1300 def function_wrap_schema(self, schema: core_schema.WrapValidatorFunctionSchema) -> JsonSchemaValue:
1301 """Generates a JSON schema that matches a function-wrap schema.
1303 Args:
1304 schema: The core schema.
1306 Returns:
1307 The generated JSON schema.
1308 """
1309 if self.mode == 'validation' and (input_schema := schema.get('json_schema_input_schema')):
1310 return self.generate_inner(input_schema)
1312 return self.generate_inner(schema['schema'])
1314 def default_schema(self, schema: core_schema.WithDefaultSchema) -> JsonSchemaValue:
1315 """Generates a JSON schema that matches a schema with a default value.
1317 Args:
1318 schema: The core schema.
1320 Returns:
1321 The generated JSON schema.
1322 """
1323 json_schema = self.generate_inner(schema['schema'])
1325 default = self.get_default_value(schema)
1326 if default is NoDefault or default is MISSING:
1327 return json_schema
1329 # we reflect the application of custom plain, no-info serializers to defaults for
1330 # JSON Schemas viewed in serialization mode:
1331 # TODO: improvements along with https://github.com/pydantic/pydantic/issues/8208
1332 if self.mode == 'serialization':
1333 # `_get_ser_schema_for_default_value()` is used to unpack potentially nested validator schemas:
1334 ser_schema = _get_ser_schema_for_default_value(schema['schema'])
1335 if (
1336 ser_schema is not None
1337 and (ser_func := ser_schema.get('function'))
1338 and not (default is None and ser_schema.get('when_used') in ('unless-none', 'json-unless-none'))
1339 ):
1340 try:
1341 default = ser_func(default) # type: ignore
1342 except Exception:
1343 # It might be that the provided default needs to be validated (read: parsed) first
1344 # (assuming `validate_default` is enabled). However, we can't perform
1345 # such validation during JSON Schema generation so we don't support
1346 # this pattern for now.
1347 # (One example is when using `foo: ByteSize = '1MB'`, which validates and
1348 # serializes as an int. In this case, `ser_func` is `int` and `int('1MB')` fails).
1349 self.emit_warning(
1350 'non-serializable-default',
1351 f'Unable to serialize value {default!r} with the plain serializer; excluding default from JSON schema',
1352 )
1353 return json_schema
1355 # Sort set/frozenset defaults to ensure deterministic JSON schema generation
1356 # We only sort if len > 1 because sets of size 0 or 1 are already deterministic
1357 if isinstance(default, collections.abc.Set) and len(default) > 1:
1358 try:
1359 default = sorted(default)
1360 except TypeError: # pragma: no cover
1361 # If items aren't comparable (e.g. mixed types), we can't sort them.
1362 pass
1364 try:
1365 encoded_default = self.encode_default(default)
1366 except pydantic_core.PydanticSerializationError:
1367 self.emit_warning(
1368 'non-serializable-default',
1369 f'Default value {default} is not JSON serializable; excluding default from JSON schema',
1370 )
1371 # Return the inner schema, as though there was no default
1372 return json_schema
1374 json_schema['default'] = encoded_default
1375 return json_schema
1377 def get_default_value(self, schema: core_schema.WithDefaultSchema) -> Any:
1378 """Get the default value to be used when generating a JSON Schema for a core schema with a default.
1380 The default implementation is to use the statically defined default value. This method can be overridden
1381 if you want to make use of the default factory.
1383 Args:
1384 schema: The `'with-default'` core schema.
1386 Returns:
1387 The default value to use, or [`NoDefault`][pydantic.json_schema.NoDefault] if no default
1388 value is available.
1389 """
1390 return schema.get('default', NoDefault)
1392 def nullable_schema(self, schema: core_schema.NullableSchema) -> JsonSchemaValue:
1393 """Generates a JSON schema that matches a schema that allows null values.
1395 Args:
1396 schema: The core schema.
1398 Returns:
1399 The generated JSON schema.
1400 """
1401 null_schema = {'type': 'null'}
1402 inner_json_schema = self.generate_inner(schema['schema'])
1404 if inner_json_schema == null_schema:
1405 return null_schema
1406 else:
1407 return self.get_union_of_schemas([inner_json_schema, null_schema])
1409 def union_schema(self, schema: core_schema.UnionSchema) -> JsonSchemaValue:
1410 """Generates a JSON schema that matches a schema that allows values matching any of the given schemas.
1412 Args:
1413 schema: The core schema.
1415 Returns:
1416 The generated JSON schema.
1417 """
1418 generated: list[JsonSchemaValue] = []
1420 for choice in core_schema.iter_union_choices(schema):
1421 try:
1422 generated.append(self.generate_inner(choice))
1423 except PydanticOmit: # noqa: PERF203
1424 continue
1425 except PydanticInvalidForJsonSchema as exc:
1426 self.emit_warning('skipped-choice', exc.message)
1427 if len(generated) == 1:
1428 return generated[0]
1429 return self.get_union_of_schemas(generated)
1431 def get_union_of_schemas(self, schemas: list[JsonSchemaValue]) -> JsonSchemaValue:
1432 """Returns the JSON Schema representation for the union of the provided JSON Schemas.
1434 The result depends on the configured `'union_format'`.
1436 Args:
1437 schemas: The list of JSON Schemas to be included in the union.
1439 Returns:
1440 The JSON Schema representing the union of schemas.
1441 """
1442 if self.union_format == 'primitive_type_array':
1443 types: list[str] = []
1444 for schema in schemas:
1445 schema_types: list[str] | str | None = schema.get('type')
1446 if schema_types is None:
1447 # No type, meaning it can be a ref or an empty schema.
1448 break
1449 if not isinstance(schema_types, list):
1450 schema_types = [schema_types]
1451 if not all(t in _PRIMITIVE_JSON_SCHEMA_TYPES for t in schema_types):
1452 break
1453 if len(schema) != 1:
1454 # We only want to include types that don't have any constraints. For instance,
1455 # if `schemas = [{'type': 'string', 'maxLength': 3}, {'type': 'string', 'minLength': 5}]`,
1456 # we don't want to produce `{'type': 'string', 'maxLength': 3, 'minLength': 5}`.
1457 # Same if we have some metadata (e.g. `title`) on a specific union member, we want to preserve it.
1458 break
1460 types.extend(schema_types)
1461 else:
1462 # If we got there, all the schemas where valid to be used with the `'primitive_type_array` format
1463 return {'type': list(dict.fromkeys(types))}
1465 return self.get_flattened_anyof(schemas)
1467 def tagged_union_schema(self, schema: core_schema.TaggedUnionSchema) -> JsonSchemaValue:
1468 """Generates a JSON schema that matches a schema that allows values matching any of the given schemas, where
1469 the schemas are tagged with a discriminator field that indicates which schema should be used to validate
1470 the value.
1472 Args:
1473 schema: The core schema.
1475 Returns:
1476 The generated JSON schema.
1477 """
1478 generated: dict[str, JsonSchemaValue] = {}
1479 for k, v in schema['choices'].items():
1480 if isinstance(k, Enum):
1481 k = k.value
1482 elif isinstance(k, bool):
1483 # Use the JSON representation so that the discriminator mapping
1484 # can be matched against the serialized payload value
1485 k = 'true' if k else 'false'
1486 elif k is None:
1487 k = 'null'
1488 try:
1489 # Use str(k) since keys must be strings for json; while not technically correct,
1490 # it's the closest that can be represented in valid JSON
1491 generated[str(k)] = self.generate_inner(v).copy()
1492 except PydanticOmit:
1493 continue
1494 except PydanticInvalidForJsonSchema as exc:
1495 self.emit_warning('skipped-choice', exc.message)
1497 one_of_choices = _deduplicate_schemas(generated.values())
1498 json_schema: JsonSchemaValue = {'oneOf': one_of_choices}
1500 # This reflects the v1 behavior; TODO: we should make it possible to exclude OpenAPI stuff from the JSON schema
1501 openapi_discriminator = self._extract_discriminator(schema, one_of_choices)
1502 if openapi_discriminator is not None:
1503 json_schema['discriminator'] = {
1504 'propertyName': openapi_discriminator,
1505 'mapping': {k: v.get('$ref', v) for k, v in generated.items()},
1506 }
1508 return json_schema
1510 def _extract_discriminator(
1511 self, schema: core_schema.TaggedUnionSchema, one_of_choices: list[JsonDict]
1512 ) -> str | None:
1513 """Extract a compatible OpenAPI discriminator from the schema and one_of choices that end up in the final
1514 schema."""
1515 openapi_discriminator: str | None = None
1517 if isinstance(schema['discriminator'], str):
1518 return schema['discriminator']
1520 if isinstance(schema['discriminator'], list):
1521 # If the discriminator is a single item list containing a string, that is equivalent to the string case
1522 if len(schema['discriminator']) == 1 and isinstance(schema['discriminator'][0], str):
1523 return schema['discriminator'][0]
1524 # When an alias is used that is different from the field name, the discriminator will be a list of single
1525 # str lists, one for the attribute and one for the actual alias. The logic here will work even if there is
1526 # more than one possible attribute, and looks for whether a single alias choice is present as a documented
1527 # property on all choices. If so, that property will be used as the OpenAPI discriminator.
1528 for alias_path in schema['discriminator']:
1529 if not isinstance(alias_path, list):
1530 break # this means that the discriminator is not a list of alias paths
1531 if len(alias_path) != 1:
1532 continue # this means that the "alias" does not represent a single field
1533 alias = alias_path[0]
1534 if not isinstance(alias, str):
1535 continue # this means that the "alias" does not represent a field
1536 alias_is_present_on_all_choices = True
1537 for choice in one_of_choices:
1538 try:
1539 choice = self.resolve_ref_schema(choice)
1540 except RuntimeError as exc:
1541 # TODO: fixme - this is a workaround for the fact that we can't always resolve refs
1542 # for tagged union choices at this point in the schema gen process, we might need to do
1543 # another pass at the end like we do for core schemas
1544 self.emit_warning('skipped-discriminator', str(exc))
1545 choice = {}
1546 properties = choice.get('properties', {})
1547 if not isinstance(properties, dict) or alias not in properties:
1548 alias_is_present_on_all_choices = False
1549 break
1550 if alias_is_present_on_all_choices:
1551 openapi_discriminator = alias
1552 break
1553 return openapi_discriminator
1555 def chain_schema(self, schema: core_schema.ChainSchema) -> JsonSchemaValue:
1556 """Generates a JSON schema that matches a core_schema.ChainSchema.
1558 When generating a schema for validation, we return the validation JSON schema for the first step in the chain.
1559 For serialization, we return the serialization JSON schema for the last step in the chain.
1561 Args:
1562 schema: The core schema.
1564 Returns:
1565 The generated JSON schema.
1566 """
1567 step_index = 0 if self.mode == 'validation' else -1 # use first step for validation, last for serialization
1568 return self.generate_inner(schema['steps'][step_index])
1570 def lax_or_strict_schema(self, schema: core_schema.LaxOrStrictSchema) -> JsonSchemaValue:
1571 """Generates a JSON schema that matches a schema that allows values matching either the lax schema or the
1572 strict schema.
1574 Args:
1575 schema: The core schema.
1577 Returns:
1578 The generated JSON schema.
1579 """
1580 # TODO: Need to read the default value off of model config or whatever
1581 use_strict = schema.get('strict', False) # TODO: replace this default False
1582 # If your JSON schema fails to generate it is probably
1583 # because one of the following two branches failed.
1584 if use_strict:
1585 return self.generate_inner(schema['strict_schema'])
1586 else:
1587 return self.generate_inner(schema['lax_schema'])
1589 def json_or_python_schema(self, schema: core_schema.JsonOrPythonSchema) -> JsonSchemaValue:
1590 """Generates a JSON schema that matches a schema that allows values matching either the JSON schema or the
1591 Python schema.
1593 The JSON schema is used instead of the Python schema. If you want to use the Python schema, you should override
1594 this method.
1596 Args:
1597 schema: The core schema.
1599 Returns:
1600 The generated JSON schema.
1601 """
1602 return self.generate_inner(schema['json_schema'])
1604 def typed_dict_schema(self, schema: core_schema.TypedDictSchema) -> JsonSchemaValue:
1605 """Generates a JSON schema that matches a schema that defines a typed dict.
1607 Args:
1608 schema: The core schema.
1610 Returns:
1611 The generated JSON schema.
1612 """
1613 total = schema.get('total', True)
1614 named_required_fields: list[tuple[str, bool, CoreSchemaField]] = [
1615 (name, self.field_is_required(field, total), field)
1616 for name, field in schema['fields'].items()
1617 if self.field_is_present(field)
1618 ]
1619 if self.mode == 'serialization':
1620 named_required_fields.extend(self._name_required_computed_fields(schema.get('computed_fields', [])))
1621 cls = schema.get('cls')
1622 config = _get_typed_dict_config(cls)
1623 with self._config_wrapper_stack.push(config):
1624 json_schema = self._named_required_fields_schema(named_required_fields)
1626 # There's some duplication between `extra_behavior` and
1627 # the config's `extra`/core config's `extra_fields_behavior`.
1628 # However, it is common to manually create TypedDictSchemas,
1629 # where you don't necessarily have a class.
1630 # At runtime, `extra_behavior` takes priority over the config
1631 # for validation, so follow the same for the JSON Schema:
1632 if 'extras_schema' in schema and schema['extras_schema'] != core_schema.any_schema():
1633 allow_additional_props = self.generate_inner(schema['extras_schema'])
1634 else:
1635 allow_additional_props = True
1637 if schema.get('extra_behavior') == 'forbid':
1638 json_schema['additionalProperties'] = False
1639 elif schema.get('extra_behavior') == 'allow':
1640 json_schema['additionalProperties'] = allow_additional_props
1642 if cls is not None:
1643 # `_update_class_schema()` will not override
1644 # `additionalProperties` if already present:
1645 self._update_class_schema(json_schema, cls, config or {})
1646 elif 'additionalProperties' not in json_schema:
1647 extra = schema.get('config', {}).get('extra_fields_behavior')
1648 if extra == 'forbid':
1649 json_schema['additionalProperties'] = False
1650 elif extra == 'allow':
1651 json_schema['additionalProperties'] = allow_additional_props
1653 return json_schema
1655 @staticmethod
1656 def _name_required_computed_fields(
1657 computed_fields: list[ComputedField],
1658 ) -> list[tuple[str, bool, core_schema.ComputedField]]:
1659 return [
1660 (field['property_name'], field.get('serialization_exclude_if') is None, field) for field in computed_fields
1661 ]
1663 def _named_required_fields_schema(
1664 self, named_required_fields: Sequence[tuple[str, bool, CoreSchemaField]]
1665 ) -> JsonSchemaValue:
1666 properties: dict[str, JsonSchemaValue] = {}
1667 required_fields: list[str] = []
1668 for name, required, field in named_required_fields:
1669 if self.by_alias:
1670 name = self._get_alias_name(field, name)
1671 try:
1672 field_json_schema = self.generate_inner(field).copy()
1673 except PydanticOmit:
1674 continue
1675 if 'title' not in field_json_schema and self.field_title_should_be_set(field):
1676 title = self.get_title_from_name(name)
1677 field_json_schema['title'] = title
1678 field_json_schema = self.handle_ref_overrides(field_json_schema)
1679 properties[name] = field_json_schema
1680 if required:
1681 required_fields.append(name)
1683 json_schema = {'type': 'object', 'properties': properties}
1684 if required_fields:
1685 json_schema['required'] = required_fields
1686 return json_schema
1688 def _get_alias_name(self, field: CoreSchemaField, name: str) -> str:
1689 if field['type'] == 'computed-field':
1690 alias: Any = field.get('alias', name)
1691 elif self.mode == 'validation':
1692 use_alias = self._config.validate_by_alias if self._by_alias is None else self._by_alias
1693 alias = field.get('validation_alias', name) if use_alias else name
1694 else:
1695 alias = field.get('serialization_alias', name)
1696 if isinstance(alias, str):
1697 name = alias
1698 elif isinstance(alias, list):
1699 alias = cast('list[str] | str', alias)
1700 for path in alias:
1701 if isinstance(path, list) and len(path) == 1 and isinstance(path[0], str):
1702 # Use the first valid single-item string path; the code that constructs the alias array
1703 # should ensure the first such item is what belongs in the JSON schema
1704 name = path[0]
1705 break
1706 else:
1707 assert_never(alias)
1708 return name
1710 def typed_dict_field_schema(self, schema: core_schema.TypedDictField) -> JsonSchemaValue:
1711 """Generates a JSON schema that matches a schema that defines a typed dict field.
1713 Args:
1714 schema: The core schema.
1716 Returns:
1717 The generated JSON schema.
1718 """
1719 return self.generate_inner(schema['schema'])
1721 def dataclass_field_schema(self, schema: core_schema.DataclassField) -> JsonSchemaValue:
1722 """Generates a JSON schema that matches a schema that defines a dataclass field.
1724 Args:
1725 schema: The core schema.
1727 Returns:
1728 The generated JSON schema.
1729 """
1730 return self.generate_inner(schema['schema'])
1732 def model_field_schema(self, schema: core_schema.ModelField) -> JsonSchemaValue:
1733 """Generates a JSON schema that matches a schema that defines a model field.
1735 Args:
1736 schema: The core schema.
1738 Returns:
1739 The generated JSON schema.
1740 """
1741 return self.generate_inner(schema['schema'])
1743 def computed_field_schema(self, schema: core_schema.ComputedField) -> JsonSchemaValue:
1744 """Generates a JSON schema that matches a schema that defines a computed field.
1746 Args:
1747 schema: The core schema.
1749 Returns:
1750 The generated JSON schema.
1751 """
1752 return self.generate_inner(schema['return_schema'])
1754 def model_schema(self, schema: core_schema.ModelSchema) -> JsonSchemaValue:
1755 """Generates a JSON schema that matches a schema that defines a model.
1757 Args:
1758 schema: The core schema.
1760 Returns:
1761 The generated JSON schema.
1762 """
1763 # We do not use schema['model'].model_json_schema() here
1764 # because it could lead to inconsistent refs handling, etc.
1765 cls = cast('type[BaseModel]', schema['cls'])
1766 config = cls.model_config
1768 with self._config_wrapper_stack.push(config):
1769 json_schema = self.generate_inner(schema['schema'])
1771 self._update_class_schema(json_schema, cls, config)
1773 return json_schema
1775 def _update_class_schema(self, json_schema: JsonSchemaValue, cls: type[Any], config: ConfigDict) -> None:
1776 """Update json_schema with the following, extracted from `config` and `cls`:
1778 * title
1779 * description
1780 * additional properties
1781 * json_schema_extra
1782 * deprecated
1784 Done in place, hence there's no return value as the original json_schema is mutated.
1785 No ref resolving is involved here, as that's not appropriate for simple updates.
1786 """
1787 from ._internal._dataclasses import is_stdlib_dataclass
1788 from .main import BaseModel
1790 if (config_title := config.get('title')) is not None:
1791 json_schema.setdefault('title', config_title)
1792 elif model_title_generator := config.get('model_title_generator'):
1793 title = model_title_generator(cls)
1794 if not isinstance(title, str):
1795 raise TypeError(f'model_title_generator {model_title_generator} must return str, not {title.__class__}')
1796 json_schema.setdefault('title', title)
1797 if 'title' not in json_schema:
1798 json_schema['title'] = cls.__name__
1800 # BaseModel and dataclasses; don't use cls.__doc__ as it will contain the verbose class signature by default
1801 if cls is BaseModel:
1802 docstring = None
1803 elif is_stdlib_dataclass(cls): # For Pydantic dataclasses, we already handle this at class creation
1804 # The `dataclass` module generates a `__doc__` based on the `inspect.signature()`
1805 # result, which we don't want to use as a description. Such `__doc__` startswith
1806 # `cls.__name__(`, which could lead to mistakenly discarding it if for some reason
1807 # an explicitly set class docstring follows the same pattern, but this is unlikely
1808 # to happen.
1809 doc = cls.__doc__
1810 docstring = None if doc is None or doc.startswith(f'{cls.__name__}(') else doc
1811 else:
1812 docstring = cls.__doc__
1814 if docstring:
1815 json_schema.setdefault('description', inspect.cleandoc(docstring))
1817 extra = config.get('extra')
1818 if 'additionalProperties' not in json_schema: # This check is particularly important for `typed_dict_schema()`
1819 if extra == 'allow':
1820 json_schema['additionalProperties'] = True
1821 elif extra == 'forbid':
1822 json_schema['additionalProperties'] = False
1824 json_schema_extra = config.get('json_schema_extra')
1825 if issubclass(cls, BaseModel) and cls.__pydantic_root_model__:
1826 root_json_schema_extra = cls.model_fields['root'].json_schema_extra
1827 if json_schema_extra and root_json_schema_extra:
1828 raise ValueError(
1829 '"model_config[\'json_schema_extra\']" and "Field.json_schema_extra" on "RootModel.root"'
1830 ' field must not be set simultaneously'
1831 )
1832 if root_json_schema_extra:
1833 json_schema_extra = root_json_schema_extra
1835 if isinstance(json_schema_extra, (staticmethod, classmethod)):
1836 # In older versions of python, this is necessary to ensure staticmethod/classmethods are callable
1837 json_schema_extra = json_schema_extra.__get__(cls)
1839 if isinstance(json_schema_extra, dict):
1840 json_schema.update(json_schema_extra)
1841 elif callable(json_schema_extra):
1842 if len(_typing_extra.signature_no_eval(json_schema_extra).parameters) > 1:
1843 json_schema_extra = cast(Callable[[JsonDict, type[Any]], None], json_schema_extra)
1844 json_schema_extra(json_schema, cls)
1845 else:
1846 json_schema_extra = cast(Callable[[JsonDict], None], json_schema_extra)
1847 json_schema_extra(json_schema)
1848 elif json_schema_extra is not None:
1849 raise ValueError(
1850 f"model_config['json_schema_extra']={json_schema_extra} should be a dict, callable, or None"
1851 )
1853 if hasattr(cls, '__deprecated__'):
1854 json_schema['deprecated'] = True
1856 def resolve_ref_schema(self, json_schema: JsonSchemaValue) -> JsonSchemaValue:
1857 """Resolve a JsonSchemaValue to the non-ref schema if it is a $ref schema.
1859 Args:
1860 json_schema: The schema to resolve.
1862 Returns:
1863 The resolved schema.
1865 Raises:
1866 RuntimeError: If the schema reference can't be found in definitions.
1867 """
1868 while '$ref' in json_schema:
1869 ref = json_schema['$ref']
1870 schema_to_update = self.get_schema_from_definitions(JsonRef(ref))
1871 if schema_to_update is None:
1872 raise RuntimeError(f'Cannot update undefined schema for $ref={ref}')
1873 json_schema = schema_to_update
1874 return json_schema
1876 def model_fields_schema(self, schema: core_schema.ModelFieldsSchema) -> JsonSchemaValue:
1877 """Generates a JSON schema that matches a schema that defines a model's fields.
1879 Args:
1880 schema: The core schema.
1882 Returns:
1883 The generated JSON schema.
1884 """
1885 named_required_fields: list[tuple[str, bool, CoreSchemaField]] = [
1886 (name, self.field_is_required(field, total=True), field)
1887 for name, field in schema['fields'].items()
1888 if self.field_is_present(field)
1889 ]
1890 if self.mode == 'serialization':
1891 named_required_fields.extend(self._name_required_computed_fields(schema.get('computed_fields', [])))
1892 json_schema = self._named_required_fields_schema(named_required_fields)
1893 extras_schema = schema.get('extras_schema', None)
1894 if extras_schema is not None:
1895 schema_to_update = self.resolve_ref_schema(json_schema)
1896 schema_to_update['additionalProperties'] = self.generate_inner(extras_schema)
1897 return json_schema
1899 def field_is_present(self, field: CoreSchemaField) -> bool:
1900 """Whether the field should be included in the generated JSON schema.
1902 Args:
1903 field: The schema for the field itself.
1905 Returns:
1906 `True` if the field should be included in the generated JSON schema, `False` otherwise.
1907 """
1908 if self.mode == 'serialization':
1909 # If you still want to include the field in the generated JSON schema,
1910 # override this method and return True
1911 return not field.get('serialization_exclude')
1912 elif self.mode == 'validation':
1913 return True
1914 else:
1915 assert_never(self.mode)
1917 def field_is_required(
1918 self,
1919 field: core_schema.ModelField | core_schema.DataclassField | core_schema.TypedDictField,
1920 total: bool,
1921 ) -> bool:
1922 """Whether the field should be marked as required in the generated JSON schema.
1923 (Note that this is irrelevant if the field is not present in the JSON schema.).
1925 Args:
1926 field: The schema for the field itself.
1927 total: Only applies to `TypedDictField`s.
1928 Indicates if the `TypedDict` this field belongs to is total, in which case any fields that don't
1929 explicitly specify `required=False` are required.
1931 Returns:
1932 `True` if the field should be marked as required in the generated JSON schema, `False` otherwise.
1933 """
1934 if field['type'] == 'typed-dict-field':
1935 required = field.get('required', total)
1936 else:
1937 required = field['schema']['type'] != 'default'
1939 if self.mode == 'serialization':
1940 has_exclude_if = field.get('serialization_exclude_if') is not None
1941 if self._config.json_schema_serialization_defaults_required:
1942 return not has_exclude_if
1943 else:
1944 return required and not has_exclude_if
1945 else:
1946 return required
1948 def dataclass_args_schema(self, schema: core_schema.DataclassArgsSchema) -> JsonSchemaValue:
1949 """Generates a JSON schema that matches a schema that defines a dataclass's constructor arguments.
1951 Args:
1952 schema: The core schema.
1954 Returns:
1955 The generated JSON schema.
1956 """
1957 named_required_fields: list[tuple[str, bool, CoreSchemaField]] = [
1958 (field['name'], self.field_is_required(field, total=True), field)
1959 for field in schema['fields']
1960 if self.field_is_present(field)
1961 ]
1962 if self.mode == 'serialization':
1963 named_required_fields.extend(self._name_required_computed_fields(schema.get('computed_fields', [])))
1964 return self._named_required_fields_schema(named_required_fields)
1966 def dataclass_schema(self, schema: core_schema.DataclassSchema) -> JsonSchemaValue:
1967 """Generates a JSON schema that matches a schema that defines a dataclass.
1969 Args:
1970 schema: The core schema.
1972 Returns:
1973 The generated JSON schema.
1974 """
1976 cls = schema['cls']
1977 # If (stdlib) dataclass doesn't have config, the parent's config is used (as during core schema generation):
1978 config = cast('ConfigDict | None', getattr(cls, '__pydantic_config__', None))
1980 with self._config_wrapper_stack.push(config):
1981 json_schema = self.generate_inner(schema['schema']).copy()
1983 # Only the parent's `extra` config value is relevant to the class schema. It is propagated
1984 # to the core config (and used for validation), so we reuse it:
1985 class_config = config or {}
1986 if 'extra' not in class_config and (extra := schema.get('config', {}).get('extra_fields_behavior')) is not None:
1987 class_config = cast('ConfigDict', {**class_config, 'extra': extra})
1989 self._update_class_schema(json_schema, cls, class_config)
1991 return json_schema
1993 def named_tuple_field_schema(self, schema: core_schema.NamedTupleField) -> JsonSchemaValue:
1994 """Generates a JSON schema that matches a schema that defines a named tuple field.
1996 Args:
1997 schema: The core schema.
1999 Returns:
2000 The generated JSON schema.
2001 """
2002 return self.generate_inner(schema['schema'])
2004 def named_tuple_schema(self, schema: core_schema.NamedTupleSchema) -> JsonSchemaValue:
2005 """Generates a JSON schema that matches a schema that defines a named tuple.
2007 Args:
2008 schema: The core schema.
2010 Returns:
2011 The generated JSON schema.
2012 """
2013 prefix_items: list[JsonSchemaValue] = []
2014 min_items = 0
2016 for field in schema['fields']:
2017 name = field['name']
2018 if self.by_alias:
2019 alias = field.get('validation_alias')
2020 if isinstance(alias, str):
2021 name = alias
2023 field_schema = self.generate_inner(field['schema']).copy()
2024 if 'title' not in field_schema and self.field_title_should_be_set(field['schema']):
2025 field_schema['title'] = self.get_title_from_name(name)
2026 prefix_items.append(field_schema)
2028 if field['schema']['type'] != 'default':
2029 # This assumes that if the field has a default value,
2030 # the inner schema must be of type WithDefaultSchema.
2031 min_items += 1
2033 json_schema: JsonSchemaValue = {'type': 'array'}
2034 if prefix_items:
2035 json_schema['prefixItems'] = prefix_items
2036 if min_items:
2037 json_schema['minItems'] = min_items
2038 json_schema['maxItems'] = len(prefix_items)
2040 return json_schema
2042 def arguments_schema(self, schema: core_schema.ArgumentsSchema) -> JsonSchemaValue:
2043 """Generates a JSON schema that matches a schema that defines a function's arguments.
2045 Args:
2046 schema: The core schema.
2048 Returns:
2049 The generated JSON schema.
2050 """
2051 prefer_positional = schema.get('metadata', {}).get('pydantic_js_prefer_positional_arguments')
2053 arguments = schema['arguments_schema']
2054 kw_only_arguments = [a for a in arguments if a.get('mode') == 'keyword_only']
2055 kw_or_p_arguments = [a for a in arguments if a.get('mode') in {'positional_or_keyword', None}]
2056 p_only_arguments = [a for a in arguments if a.get('mode') == 'positional_only']
2057 var_args_schema = schema.get('var_args_schema')
2058 var_kwargs_schema = schema.get('var_kwargs_schema')
2060 if prefer_positional:
2061 positional_possible = not kw_only_arguments and not var_kwargs_schema
2062 if positional_possible:
2063 return self.p_arguments_schema(p_only_arguments + kw_or_p_arguments, var_args_schema)
2065 keyword_possible = not p_only_arguments and not var_args_schema
2066 if keyword_possible:
2067 return self.kw_arguments_schema(kw_or_p_arguments + kw_only_arguments, var_kwargs_schema)
2069 if not prefer_positional:
2070 positional_possible = not kw_only_arguments and not var_kwargs_schema
2071 if positional_possible:
2072 return self.p_arguments_schema(p_only_arguments + kw_or_p_arguments, var_args_schema)
2074 raise PydanticInvalidForJsonSchema(
2075 'Unable to generate JSON schema for arguments validator with positional-only and keyword-only arguments'
2076 )
2078 def kw_arguments_schema(
2079 self, arguments: list[core_schema.ArgumentsParameter], var_kwargs_schema: CoreSchema | None
2080 ) -> JsonSchemaValue:
2081 """Generates a JSON schema that matches a schema that defines a function's keyword arguments.
2083 Args:
2084 arguments: The core schema.
2086 Returns:
2087 The generated JSON schema.
2088 """
2089 properties: dict[str, JsonSchemaValue] = {}
2090 required: list[str] = []
2091 for argument in arguments:
2092 name = self.get_argument_name(argument)
2093 argument_schema = self.generate_inner(argument['schema']).copy()
2094 if 'title' not in argument_schema and self.field_title_should_be_set(argument['schema']):
2095 argument_schema['title'] = self.get_title_from_name(name)
2096 properties[name] = argument_schema
2098 if argument['schema']['type'] != 'default':
2099 # This assumes that if the argument has a default value,
2100 # the inner schema must be of type WithDefaultSchema.
2101 # I believe this is true, but I am not 100% sure
2102 required.append(name)
2104 json_schema: JsonSchemaValue = {'type': 'object', 'properties': properties}
2105 if required:
2106 json_schema['required'] = required
2108 if var_kwargs_schema:
2109 additional_properties_schema = self.generate_inner(var_kwargs_schema)
2110 if additional_properties_schema:
2111 json_schema['additionalProperties'] = additional_properties_schema
2112 else:
2113 json_schema['additionalProperties'] = False
2114 return json_schema
2116 def p_arguments_schema(
2117 self, arguments: list[core_schema.ArgumentsParameter], var_args_schema: CoreSchema | None
2118 ) -> JsonSchemaValue:
2119 """Generates a JSON schema that matches a schema that defines a function's positional arguments.
2121 Args:
2122 arguments: The core schema.
2124 Returns:
2125 The generated JSON schema.
2126 """
2127 prefix_items: list[JsonSchemaValue] = []
2128 min_items = 0
2130 for argument in arguments:
2131 name = self.get_argument_name(argument)
2133 argument_schema = self.generate_inner(argument['schema']).copy()
2134 if 'title' not in argument_schema and self.field_title_should_be_set(argument['schema']):
2135 argument_schema['title'] = self.get_title_from_name(name)
2136 prefix_items.append(argument_schema)
2138 if argument['schema']['type'] != 'default':
2139 # This assumes that if the argument has a default value,
2140 # the inner schema must be of type WithDefaultSchema.
2141 # I believe this is true, but I am not 100% sure
2142 min_items += 1
2144 json_schema: JsonSchemaValue = {'type': 'array'}
2145 if prefix_items:
2146 json_schema['prefixItems'] = prefix_items
2147 if min_items:
2148 json_schema['minItems'] = min_items
2150 if var_args_schema:
2151 items_schema = self.generate_inner(var_args_schema)
2152 if items_schema:
2153 json_schema['items'] = items_schema
2154 else:
2155 json_schema['maxItems'] = len(prefix_items)
2157 return json_schema
2159 def get_argument_name(self, argument: core_schema.ArgumentsParameter | core_schema.ArgumentsV3Parameter) -> str:
2160 """Retrieves the name of an argument.
2162 Args:
2163 argument: The core schema.
2165 Returns:
2166 The name of the argument.
2167 """
2168 name = argument['name']
2169 if self.by_alias:
2170 alias = argument.get('alias')
2171 if isinstance(alias, str):
2172 name = alias
2173 else:
2174 pass # might want to do something else?
2175 return name
2177 def arguments_v3_schema(self, schema: core_schema.ArgumentsV3Schema) -> JsonSchemaValue:
2178 """Generates a JSON schema that matches a schema that defines a function's arguments.
2180 Args:
2181 schema: The core schema.
2183 Returns:
2184 The generated JSON schema.
2185 """
2186 arguments = schema['arguments_schema']
2187 properties: dict[str, JsonSchemaValue] = {}
2188 required: list[str] = []
2189 for argument in arguments:
2190 mode = argument.get('mode', 'positional_or_keyword')
2191 name = self.get_argument_name(argument)
2192 argument_schema = self.generate_inner(argument['schema']).copy()
2193 if mode == 'var_args':
2194 argument_schema = {'type': 'array', 'items': argument_schema}
2195 elif mode == 'var_kwargs_uniform':
2196 argument_schema = {'type': 'object', 'additionalProperties': argument_schema}
2198 argument_schema.setdefault('title', self.get_title_from_name(name))
2199 properties[name] = argument_schema
2201 if (
2202 (mode == 'var_kwargs_unpacked_typed_dict' and 'required' in argument_schema)
2203 or mode not in {'var_args', 'var_kwargs_uniform', 'var_kwargs_unpacked_typed_dict'}
2204 and argument['schema']['type'] != 'default'
2205 ):
2206 # This assumes that if the argument has a default value,
2207 # the inner schema must be of type WithDefaultSchema.
2208 # I believe this is true, but I am not 100% sure
2209 required.append(name)
2211 json_schema: JsonSchemaValue = {'type': 'object', 'properties': properties}
2212 if required:
2213 json_schema['required'] = required
2214 return json_schema
2216 def call_schema(self, schema: core_schema.CallSchema) -> JsonSchemaValue:
2217 """Generates a JSON schema that matches a schema that defines a function call.
2219 Args:
2220 schema: The core schema.
2222 Returns:
2223 The generated JSON schema.
2224 """
2225 return self.generate_inner(schema['arguments_schema'])
2227 def custom_error_schema(self, schema: core_schema.CustomErrorSchema) -> JsonSchemaValue:
2228 """Generates a JSON schema that matches a schema that defines a custom error.
2230 Args:
2231 schema: The core schema.
2233 Returns:
2234 The generated JSON schema.
2235 """
2236 return self.generate_inner(schema['schema'])
2238 def json_schema(self, schema: core_schema.JsonSchema) -> JsonSchemaValue:
2239 """Generates a JSON schema that matches a schema that defines a JSON object.
2241 Args:
2242 schema: The core schema.
2244 Returns:
2245 The generated JSON schema.
2246 """
2247 content_core_schema = schema.get('schema') or core_schema.any_schema()
2248 content_json_schema = self.generate_inner(content_core_schema)
2249 if self.mode == 'validation':
2250 return {'type': 'string', 'contentMediaType': 'application/json', 'contentSchema': content_json_schema}
2251 else:
2252 # self.mode == 'serialization'
2253 return content_json_schema
2255 def url_schema(self, schema: core_schema.UrlSchema) -> JsonSchemaValue:
2256 """Generates a JSON schema that matches a schema that defines a URL.
2258 Args:
2259 schema: The core schema.
2261 Returns:
2262 The generated JSON schema.
2263 """
2264 json_schema = {'type': 'string', 'format': 'uri', 'minLength': 1}
2265 self.update_with_validations(json_schema, schema, self.ValidationsMapping.string)
2266 return json_schema
2268 def multi_host_url_schema(self, schema: core_schema.MultiHostUrlSchema) -> JsonSchemaValue:
2269 """Generates a JSON schema that matches a schema that defines a URL that can be used with multiple hosts.
2271 Args:
2272 schema: The core schema.
2274 Returns:
2275 The generated JSON schema.
2276 """
2277 # Note: 'multi-host-uri' is a custom/pydantic-specific format, not part of the JSON Schema spec
2278 json_schema = {'type': 'string', 'format': 'multi-host-uri', 'minLength': 1}
2279 self.update_with_validations(json_schema, schema, self.ValidationsMapping.string)
2280 return json_schema
2282 def uuid_schema(self, schema: core_schema.UuidSchema) -> JsonSchemaValue:
2283 """Generates a JSON schema that matches a UUID.
2285 Args:
2286 schema: The core schema.
2288 Returns:
2289 The generated JSON schema.
2290 """
2291 return {'type': 'string', 'format': 'uuid'}
2293 def definitions_schema(self, schema: core_schema.DefinitionsSchema) -> JsonSchemaValue:
2294 """Generates a JSON schema that matches a schema that defines a JSON object with definitions.
2296 Args:
2297 schema: The core schema.
2299 Returns:
2300 The generated JSON schema.
2301 """
2302 for definition in schema['definitions']:
2303 try:
2304 self.generate_inner(definition)
2305 except PydanticInvalidForJsonSchema as e: # noqa: PERF203
2306 core_ref: CoreRef = CoreRef(definition['ref']) # type: ignore
2307 self._core_defs_invalid_for_json_schema[self.get_defs_ref((core_ref, self.mode))] = e
2308 continue
2309 return self.generate_inner(schema['schema'])
2311 def definition_ref_schema(self, schema: core_schema.DefinitionReferenceSchema) -> JsonSchemaValue:
2312 """Generates a JSON schema that matches a schema that references a definition.
2314 Args:
2315 schema: The core schema.
2317 Returns:
2318 The generated JSON schema.
2319 """
2320 core_ref = CoreRef(schema['schema_ref'])
2321 _, ref_json_schema = self.get_cache_defs_ref_schema(core_ref)
2322 return ref_json_schema
2324 def ser_schema(
2325 self, schema: core_schema.SerSchema | core_schema.IncExSeqSerSchema | core_schema.IncExDictSerSchema
2326 ) -> JsonSchemaValue | None:
2327 """Generates a JSON schema that matches a schema that defines a serialized object.
2329 Args:
2330 schema: The core schema.
2332 Returns:
2333 The generated JSON schema.
2334 """
2335 match schema:
2336 case {'type': 'function-plain' | 'function-wrap'}:
2337 # PlainSerializerFunctionSerSchema or WrapSerializerFunctionSerSchema
2338 return_schema = schema.get('return_schema')
2339 if return_schema is not None:
2340 return self.generate_inner(return_schema)
2341 case {'type': 'format' | 'to-string'}:
2342 # FormatSerSchema or ToStringSerSchema
2343 return self.str_schema(core_schema.str_schema())
2344 case {'type': 'model'}:
2345 # ModelSerSchema
2346 return self.generate_inner(schema['schema'])
2347 return None
2349 def complex_schema(self, schema: core_schema.ComplexSchema) -> JsonSchemaValue:
2350 """Generates a JSON schema that matches a complex number.
2352 JSON has no standard way to represent complex numbers. Complex number is not a numeric
2353 type. Here we represent complex number as strings following the rule defined by Python.
2354 For instance, '1+2j' is an accepted complex string. Details can be found in
2355 [Python's `complex` documentation][complex].
2357 Args:
2358 schema: The core schema.
2360 Returns:
2361 The generated JSON schema.
2362 """
2363 return {'type': 'string'}
2365 # ### Utility methods
2367 def get_title_from_name(self, name: str) -> str:
2368 """Retrieves a title from a name.
2370 Args:
2371 name: The name to retrieve a title from.
2373 Returns:
2374 The title.
2375 """
2376 return name.title().replace('_', ' ').strip()
2378 def field_title_should_be_set(self, schema: CoreSchemaOrField) -> bool:
2379 """Returns true if a field with the given schema should have a title set based on the field name.
2381 Intuitively, we want this to return true for schemas that wouldn't otherwise provide their own title
2382 (e.g., int, float, str), and false for those that would (e.g., BaseModel subclasses).
2384 Args:
2385 schema: The schema to check.
2387 Returns:
2388 `True` if the field should have a title set, `False` otherwise.
2389 """
2390 if _core_utils.is_core_schema_field(schema):
2391 if schema['type'] == 'computed-field':
2392 field_schema = schema['return_schema']
2393 else:
2394 field_schema = schema['schema']
2395 return self.field_title_should_be_set(field_schema)
2397 elif _core_utils.is_core_schema(schema):
2398 if schema.get('ref'): # things with refs, such as models and enums, should not have titles set
2399 return False
2400 if schema['type'] in {'default', 'nullable', 'definitions'}:
2401 return self.field_title_should_be_set(schema['schema']) # type: ignore[typeddict-item]
2402 if schema['type'] == 'missing-sentinel' and (inner_schema := schema.get('schema')) is not None:
2403 return self.field_title_should_be_set(inner_schema)
2404 if _core_utils.is_function_with_inner_schema(schema):
2405 return self.field_title_should_be_set(schema['schema'])
2406 if schema['type'] == 'definition-ref':
2407 # Referenced schemas should not have titles set for the same reason
2408 # schemas with refs should not
2409 return False
2410 return True # anything else should have title set
2412 else:
2413 raise PydanticInvalidForJsonSchema(f'Unexpected schema type: schema={schema}') # pragma: no cover
2415 def normalize_name(self, name: str) -> str:
2416 """Normalizes a name to be used as a key in a dictionary.
2418 Args:
2419 name: The name to normalize.
2421 Returns:
2422 The normalized name.
2423 """
2424 return re.sub(r'[^a-zA-Z0-9.\-_]', '_', name).replace('.', '__')
2426 def get_defs_ref(self, core_mode_ref: CoreModeRef) -> DefsRef:
2427 """Override this method to change the way that definitions keys are generated from a core reference.
2429 Args:
2430 core_mode_ref: The core reference.
2432 Returns:
2433 The definitions key.
2434 """
2435 # Split the core ref into "components"; generic origins and arguments are each separate components
2436 core_ref, mode = core_mode_ref
2437 components = re.split(r'([\][,])', core_ref)
2438 # Remove IDs from each component
2439 components = [x.rsplit(':', 1)[0] for x in components]
2440 core_ref_no_id = ''.join(components)
2441 # Remove everything before the last period from each "component"
2442 components = [re.sub(r'(?:[^.[\]]+\.)+((?:[^.[\]]+))', r'\1', x) for x in components]
2443 short_ref = ''.join(components)
2445 mode_title = _MODE_TITLE_MAPPING[mode]
2447 # It is important that the generated defs_ref values be such that at least one choice will not
2448 # be generated for any other core_ref. Currently, this should be the case because we include
2449 # the id of the source type in the core_ref
2450 name = DefsRef(self.normalize_name(short_ref))
2451 name_mode = DefsRef(self.normalize_name(short_ref) + f'-{mode_title}')
2452 module_qualname = DefsRef(self.normalize_name(core_ref_no_id))
2453 module_qualname_mode = DefsRef(f'{module_qualname}-{mode_title}')
2454 module_qualname_id = DefsRef(self.normalize_name(core_ref))
2455 occurrence_index = self._collision_index.get(module_qualname_id)
2456 if occurrence_index is None:
2457 self._collision_counter[module_qualname] += 1
2458 occurrence_index = self._collision_index[module_qualname_id] = self._collision_counter[module_qualname]
2460 module_qualname_occurrence = DefsRef(f'{module_qualname}__{occurrence_index}')
2461 module_qualname_occurrence_mode = DefsRef(f'{module_qualname_mode}__{occurrence_index}')
2463 self._prioritized_defsref_choices[module_qualname_occurrence_mode] = [
2464 name,
2465 name_mode,
2466 module_qualname,
2467 module_qualname_mode,
2468 module_qualname_occurrence,
2469 module_qualname_occurrence_mode,
2470 ]
2472 return module_qualname_occurrence_mode
2474 def get_cache_defs_ref_schema(self, core_ref: CoreRef) -> tuple[DefsRef, JsonSchemaValue]:
2475 """This method wraps the get_defs_ref method with some cache-lookup/population logic,
2476 and returns both the produced defs_ref and the JSON schema that will refer to the right definition.
2478 Args:
2479 core_ref: The core reference to get the definitions reference for.
2481 Returns:
2482 A tuple of the definitions reference and the JSON schema that will refer to it.
2483 """
2484 core_mode_ref = (core_ref, self.mode)
2485 maybe_defs_ref = self.core_to_defs_refs.get(core_mode_ref)
2486 if maybe_defs_ref is not None:
2487 json_ref = self.core_to_json_refs[core_mode_ref]
2488 return maybe_defs_ref, {'$ref': json_ref}
2490 defs_ref = self.get_defs_ref(core_mode_ref)
2492 # populate the ref translation mappings
2493 self.core_to_defs_refs[core_mode_ref] = defs_ref
2494 self.defs_to_core_refs[defs_ref] = core_mode_ref
2496 json_ref = JsonRef(self.ref_template.format(model=defs_ref))
2497 self.core_to_json_refs[core_mode_ref] = json_ref
2498 self.json_to_defs_refs[json_ref] = defs_ref
2499 ref_json_schema = {'$ref': json_ref}
2500 return defs_ref, ref_json_schema
2502 def handle_ref_overrides(self, json_schema: JsonSchemaValue) -> JsonSchemaValue:
2503 """Remove any sibling keys that are redundant with the referenced schema.
2505 Args:
2506 json_schema: The schema to remove redundant sibling keys from.
2508 Returns:
2509 The schema with redundant sibling keys removed.
2510 """
2511 if '$ref' in json_schema:
2512 # prevent modifications to the input; this copy may be safe to drop if there is significant overhead
2513 json_schema = json_schema.copy()
2515 referenced_json_schema = self.get_schema_from_definitions(JsonRef(json_schema['$ref']))
2516 if referenced_json_schema is None:
2517 # This can happen when building schemas for models with not-yet-defined references.
2518 # It may be a good idea to do a recursive pass at the end of the generation to remove
2519 # any redundant override keys.
2520 return json_schema
2521 for k, v in list(json_schema.items()):
2522 if k == '$ref':
2523 continue
2524 if k in referenced_json_schema and referenced_json_schema[k] == v:
2525 del json_schema[k] # redundant key
2527 return json_schema
2529 def get_schema_from_definitions(self, json_ref: JsonRef) -> JsonSchemaValue | None:
2530 try:
2531 def_ref = self.json_to_defs_refs[json_ref]
2532 if def_ref in self._core_defs_invalid_for_json_schema:
2533 raise self._core_defs_invalid_for_json_schema[def_ref]
2534 return self.definitions.get(def_ref, None)
2535 except KeyError:
2536 if json_ref.startswith(('http://', 'https://')):
2537 return None
2538 raise
2540 def encode_default(self, dft: Any) -> Any:
2541 """Encode a default value to a JSON-serializable value.
2543 This is used to encode default values for fields in the generated JSON schema.
2545 Args:
2546 dft: The default value to encode.
2548 Returns:
2549 The encoded default value.
2550 """
2551 from .type_adapter import TypeAdapter, _type_has_config
2553 config = self._config.config_dict
2554 if self.mode == 'validation' and ('ser_json_temporal' in config or 'ser_json_timedelta' in config):
2555 # Temporal serialization formats aren't applied to validation JSON Schemas (see `_common_temporal_schema()`),
2556 # so they shouldn't be applied to the default either:
2557 config = cast(
2558 'ConfigDict',
2559 {k: v for k, v in config.items() if k not in ('ser_json_temporal', 'ser_json_timedelta')},
2560 )
2561 try:
2562 # Types with their own config (e.g. models) can't be used with a config, so we serialize "as Any",
2563 # which will serialize using their own serializer.
2564 return TypeAdapter(Any if _type_has_config(type(dft)) else type(dft), config=config).dump_python(
2565 dft, by_alias=self.by_alias, mode='json'
2566 )
2567 except Exception as e:
2568 raise pydantic_core.PydanticSerializationError(f'Unable to encode default value {dft}') from e
2570 def update_with_validations(
2571 self, json_schema: JsonSchemaValue, core_schema: CoreSchema, mapping: dict[str, str]
2572 ) -> None:
2573 """Update the json_schema with the corresponding validations specified in the core_schema,
2574 using the provided mapping to translate keys in core_schema to the appropriate keys for a JSON schema.
2576 Args:
2577 json_schema: The JSON schema to update.
2578 core_schema: The core schema to get the validations from.
2579 mapping: A mapping from core_schema attribute names to the corresponding JSON schema attribute names.
2580 """
2581 for core_key, json_schema_key in mapping.items():
2582 if core_key in core_schema:
2583 json_schema[json_schema_key] = core_schema[core_key]
2585 class ValidationsMapping:
2586 """This class just contains mappings from core_schema attribute names to the corresponding
2587 JSON schema attribute names. While I suspect it is unlikely to be necessary, you can in
2588 principle override this class in a subclass of GenerateJsonSchema (by inheriting from
2589 GenerateJsonSchema.ValidationsMapping) to change these mappings.
2590 """
2592 numeric = {
2593 'multiple_of': 'multipleOf',
2594 'le': 'maximum',
2595 'ge': 'minimum',
2596 'lt': 'exclusiveMaximum',
2597 'gt': 'exclusiveMinimum',
2598 }
2599 bytes = {
2600 'min_length': 'minLength',
2601 'max_length': 'maxLength',
2602 }
2603 string = {
2604 'min_length': 'minLength',
2605 'max_length': 'maxLength',
2606 'pattern': 'pattern',
2607 }
2608 array = {
2609 'min_length': 'minItems',
2610 'max_length': 'maxItems',
2611 }
2612 object = {
2613 'min_length': 'minProperties',
2614 'max_length': 'maxProperties',
2615 }
2617 def get_flattened_anyof(self, schemas: list[JsonSchemaValue]) -> JsonSchemaValue:
2618 members = []
2619 for schema in schemas:
2620 if len(schema) == 1 and 'anyOf' in schema:
2621 members.extend(schema['anyOf'])
2622 else:
2623 members.append(schema)
2624 members = _deduplicate_schemas(members)
2625 if len(members) == 1:
2626 return members[0]
2627 return {'anyOf': members}
2629 def get_json_ref_counts(self, json_schema: JsonSchemaValue) -> dict[JsonRef, int]:
2630 """Get all values corresponding to the key '$ref' anywhere in the json_schema."""
2631 json_refs: dict[JsonRef, int] = Counter()
2633 def _add_json_refs(schema: Any) -> None:
2634 if isinstance(schema, dict):
2635 if '$ref' in schema:
2636 json_ref = JsonRef(schema['$ref'])
2637 if not isinstance(json_ref, str):
2638 return # in this case, '$ref' might have been the name of a property
2639 already_visited = json_ref in json_refs
2640 json_refs[json_ref] += 1
2641 if already_visited:
2642 return # prevent recursion on a definition that was already visited
2643 try:
2644 defs_ref = self.json_to_defs_refs[json_ref]
2645 if defs_ref in self._core_defs_invalid_for_json_schema:
2646 raise self._core_defs_invalid_for_json_schema[defs_ref]
2647 _add_json_refs(self.definitions[defs_ref])
2648 except KeyError:
2649 if not json_ref.startswith(('http://', 'https://')):
2650 raise
2652 for k, v in schema.items():
2653 if k == 'examples' and isinstance(v, list):
2654 # Skip examples that may contain arbitrary values and references
2655 # (see the comment in `_get_all_json_refs` for more details).
2656 continue
2657 _add_json_refs(v)
2658 elif isinstance(schema, list):
2659 for v in schema:
2660 _add_json_refs(v)
2662 _add_json_refs(json_schema)
2663 return json_refs
2665 def handle_invalid_for_json_schema(self, schema: CoreSchemaOrField, error_info: str) -> JsonSchemaValue:
2666 raise PydanticInvalidForJsonSchema(f'Cannot generate a JsonSchema for {error_info}')
2668 def emit_warning(self, kind: JsonSchemaWarningKind, detail: str) -> None:
2669 """This method simply emits PydanticJsonSchemaWarnings based on handling in the `warning_message` method."""
2670 message = self.render_warning_message(kind, detail)
2671 if message is not None:
2672 warnings.warn(message, PydanticJsonSchemaWarning)
2674 def render_warning_message(self, kind: JsonSchemaWarningKind, detail: str) -> str | None:
2675 """This method is responsible for ignoring warnings as desired, and for formatting the warning messages.
2677 You can override the value of `ignored_warning_kinds` in a subclass of GenerateJsonSchema
2678 to modify what warnings are generated. If you want more control, you can override this method;
2679 just return None in situations where you don't want warnings to be emitted.
2681 Args:
2682 kind: The kind of warning to render. It can be one of the following:
2684 - 'skipped-choice': A choice field was skipped because it had no valid choices.
2685 - 'non-serializable-default': A default value was skipped because it was not JSON-serializable.
2686 detail: A string with additional details about the warning.
2688 Returns:
2689 The formatted warning message, or `None` if no warning should be emitted.
2690 """
2691 if kind in self.ignored_warning_kinds:
2692 return None
2693 return f'{detail} [{kind}]'
2695 def _build_definitions_remapping(self) -> _DefinitionsRemapping:
2696 defs_to_json: dict[DefsRef, JsonRef] = {}
2697 for defs_refs in self._prioritized_defsref_choices.values():
2698 for defs_ref in defs_refs:
2699 json_ref = JsonRef(self.ref_template.format(model=defs_ref))
2700 defs_to_json[defs_ref] = json_ref
2702 return _DefinitionsRemapping.from_prioritized_choices(
2703 self._prioritized_defsref_choices, defs_to_json, self.definitions
2704 )
2706 def _garbage_collect_definitions(self, schema: JsonSchemaValue) -> None:
2707 visited_defs_refs: set[DefsRef] = set()
2708 unvisited_json_refs = _get_all_json_refs(schema)
2709 while unvisited_json_refs:
2710 next_json_ref = unvisited_json_refs.pop()
2711 try:
2712 next_defs_ref = self.json_to_defs_refs[next_json_ref]
2713 if next_defs_ref in visited_defs_refs:
2714 continue
2715 visited_defs_refs.add(next_defs_ref)
2716 unvisited_json_refs.update(_get_all_json_refs(self.definitions[next_defs_ref]))
2717 except KeyError:
2718 if not next_json_ref.startswith(('http://', 'https://')):
2719 raise
2721 self.definitions = {k: v for k, v in self.definitions.items() if k in visited_defs_refs}
2724# ##### Start JSON Schema Generation Functions #####
2727def model_json_schema(
2728 cls: type[BaseModel] | type[PydanticDataclass],
2729 by_alias: bool | None = None,
2730 ref_template: str = DEFAULT_REF_TEMPLATE,
2731 union_format: Literal['any_of', 'primitive_type_array'] = 'any_of',
2732 schema_generator: type[GenerateJsonSchema] = GenerateJsonSchema,
2733 mode: JsonSchemaMode = 'validation',
2734) -> dict[str, Any]:
2735 """Utility function to generate a JSON Schema for a model.
2737 Args:
2738 cls: The model class to generate a JSON Schema for.
2739 by_alias: Whether to use field aliases as keys in the generated JSON Schema. If `None` (the default),
2740 aliases are used unless disabled by the configuration (i.e. `validate_by_alias=False` for validation
2741 schemas). An explicit value takes precedence over the configuration.
2742 ref_template: The template to use for generating JSON Schema references.
2743 union_format: The format to use when combining schemas from unions together. Can be one of:
2745 - `'any_of'`: Use the [`anyOf`](https://json-schema.org/understanding-json-schema/reference/combining#anyOf)
2746 keyword to combine schemas (the default).
2747 - `'primitive_type_array'`: Use the [`type`](https://json-schema.org/understanding-json-schema/reference/type)
2748 keyword as an array of strings, containing each type of the combination. If any of the schemas is not a primitive
2749 type (`string`, `boolean`, `null`, `integer` or `number`) or contains constraints/metadata, falls back to
2750 `any_of`.
2751 schema_generator: The class to use for generating the JSON Schema.
2752 mode: The mode to use for generating the JSON Schema. It can be one of the following:
2754 - 'validation': Generate a JSON Schema for validating data.
2755 - 'serialization': Generate a JSON Schema for serializing data.
2757 Returns:
2758 The generated JSON Schema.
2759 """
2760 from .main import BaseModel
2762 schema_generator_instance = schema_generator(
2763 by_alias=by_alias, ref_template=ref_template, union_format=union_format
2764 )
2766 if isinstance(cls.__pydantic_core_schema__, _mock_val_ser.MockCoreSchema):
2767 cls.__pydantic_core_schema__.rebuild()
2769 if cls is BaseModel:
2770 raise AttributeError('model_json_schema() must be called on a subclass of BaseModel, not BaseModel itself.')
2772 assert not isinstance(cls.__pydantic_core_schema__, _mock_val_ser.MockCoreSchema), 'this is a bug! please report it'
2773 return schema_generator_instance.generate(cls.__pydantic_core_schema__, mode=mode)
2776def models_json_schema(
2777 models: Sequence[tuple[type[BaseModel] | type[PydanticDataclass], JsonSchemaMode]],
2778 *,
2779 by_alias: bool | None = None,
2780 title: str | None = None,
2781 description: str | None = None,
2782 ref_template: str = DEFAULT_REF_TEMPLATE,
2783 union_format: Literal['any_of', 'primitive_type_array'] = 'any_of',
2784 schema_generator: type[GenerateJsonSchema] = GenerateJsonSchema,
2785) -> tuple[dict[tuple[type[BaseModel] | type[PydanticDataclass], JsonSchemaMode], JsonSchemaValue], JsonSchemaValue]:
2786 """Utility function to generate a JSON Schema for multiple models.
2788 Args:
2789 models: A sequence of tuples of the form (model, mode).
2790 by_alias: Whether to use field aliases as keys in the generated JSON Schema. If `None` (the default),
2791 aliases are used unless disabled by the configuration (i.e. `validate_by_alias=False` for validation
2792 schemas). An explicit value takes precedence over the configuration.
2793 title: The title of the generated JSON Schema.
2794 description: The description of the generated JSON Schema.
2795 ref_template: The reference template to use for generating JSON Schema references.
2796 union_format: The format to use when combining schemas from unions together. Can be one of:
2798 - `'any_of'`: Use the [`anyOf`](https://json-schema.org/understanding-json-schema/reference/combining#anyOf)
2799 keyword to combine schemas (the default).
2800 - `'primitive_type_array'`: Use the [`type`](https://json-schema.org/understanding-json-schema/reference/type)
2801 keyword as an array of strings, containing each type of the combination. If any of the schemas is not a primitive
2802 type (`string`, `boolean`, `null`, `integer` or `number`) or contains constraints/metadata, falls back to
2803 `any_of`.
2804 schema_generator: The schema generator to use for generating the JSON Schema.
2806 Returns:
2807 A tuple where:
2808 - The first element is a dictionary whose keys are tuples of JSON schema key type and JSON mode, and
2809 whose values are the JSON schema corresponding to that pair of inputs. (These schemas may have
2810 JsonRef references to definitions that are defined in the second returned element.)
2811 - The second element is a JSON schema containing all definitions referenced in the first returned
2812 element, along with the optional title and description keys.
2813 """
2814 for cls, _ in models:
2815 if isinstance(cls.__pydantic_core_schema__, _mock_val_ser.MockCoreSchema):
2816 cls.__pydantic_core_schema__.rebuild()
2818 instance = schema_generator(by_alias=by_alias, ref_template=ref_template, union_format=union_format)
2819 inputs: list[tuple[type[BaseModel] | type[PydanticDataclass], JsonSchemaMode, CoreSchema]] = [
2820 (m, mode, m.__pydantic_core_schema__) for m, mode in models
2821 ]
2822 json_schemas_map, definitions = instance.generate_definitions(inputs)
2824 json_schema: dict[str, Any] = {}
2825 if definitions:
2826 json_schema['$defs'] = definitions
2827 if title:
2828 json_schema['title'] = title
2829 if description:
2830 json_schema['description'] = description
2832 return json_schemas_map, json_schema
2835# ##### End JSON Schema Generation Functions #####
2838# Decimal JSON Schema pattern utilities.
2839# Note: while we could use `\d` to represent digits (`Decimal` objects allow Unicode decimal digits), we use [0-9] instead:
2840# - to reduce complexity (`\d` expands to a large set, and with `max_digits`/`decimal_places` constraints set, it can blow
2841# the compile limit on some engines).
2842# - [0-9] is a portable pattern (e.g. `\d` is equivalent to `[0-9]` in ECMA (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Character_class_escape#d)
2843# while it allows any Unicode decimal digit in Python.
2846def _repeat(atom: str, min_count: int, max_count: int) -> str:
2847 """Build a regex repetition of `atom` between `min_count` and `max_count` times (inclusive), using the shortest quantifier."""
2848 if min_count > max_count or max_count == 0:
2849 return ''
2850 if min_count == max_count:
2851 return atom if min_count == 1 else f'{atom}{{{min_count}}}'
2852 if (min_count, max_count) == (0, 1):
2853 return f'{atom}?'
2854 return f'{atom}{{{min_count},{max_count}}}'
2857def _int_range_pattern(max_value: int) -> str:
2858 """Build a regex matching the integers between 1 and `max_value` (inclusive), without leading zeros."""
2859 digits = str(max_value)
2860 length = len(digits)
2861 alts: list[str] = [f'[1-9]{_repeat("[0-9]", k - 1, k - 1)}' for k in range(1, length)]
2862 for i, char in enumerate(digits):
2863 d = int(char)
2864 low = 1 if i == 0 else 0
2865 high = d if i == length - 1 else d - 1
2866 if high >= low:
2867 char_class = str(low) if low == high else f'[{low}-{high}]'
2868 alts.append(f'{digits[:i]}{char_class}{_repeat("[0-9]", length - i - 1, length - i - 1)}')
2869 return alts[0] if len(alts) == 1 else f'(?:{"|".join(alts)})'
2872def _decimal_validation_pattern(max_digits: int | None, decimal_places: int | None, allow_inf_nan: bool) -> str:
2873 """Build the decimal pattern for the `'validation'` mode."""
2875 number = r'(?:[0-9]+\.?[0-9]*|\.[0-9]+)' # e.g. 1, 1., .5
2876 exponent = r'[eE][+-]?[0-9]+' # e.g. e5, E5, e-7
2877 # e.g. Infinity, inf, NaN, sNaN, NaN123 (case-insensitive). Spelled out explicitly, as inline flags
2878 # aren't supported by all regular expression engines:
2879 non_finite = r'|[iI][nN][fF](?:[iI][nN][iI][tT][yY])?|[sS]?[nN][aA][nN][0-9]*' if allow_inf_nan else ''
2880 if max_digits is None and decimal_places is None:
2881 return rf'^[+-]?(?:{number}(?:{exponent})?{non_finite})$'
2883 alts: list[str] = []
2884 if max_digits is not None and decimal_places is not None:
2885 # If `decimal_places > max_digits`, no whole digits are allowed and `max_digits` bounds the decimal places:
2886 whole = max(0, max_digits - decimal_places)
2887 places = min(max_digits, decimal_places)
2888 int_part = f'0*{_repeat("[0-9]", 1, whole)}' if whole else '0+'
2889 opt_int_part = f'0*{_repeat("[0-9]", 0, whole)}'
2890 opt_frac_part = rf'\.{_repeat("[0-9]", 0, places)}0*'
2891 frac_part = rf'\.{_repeat("[0-9]", 1, places)}0*' if places else r'\.0+'
2892 alts += [f'{int_part}(?:{opt_frac_part})?', f'{opt_int_part}{frac_part}']
2893 elif max_digits is not None:
2894 # The whole part and decimal part must have at most `max_digits` digits in total,
2895 # so we enumerate the possible widths of the whole part:
2896 int_part = f'0*{_repeat("[0-9]", 1, max_digits)}' if max_digits else '0+'
2897 alts.append(rf'{int_part}(?:\.0*)?')
2898 alts += [
2899 rf'0*{_repeat("[0-9]", whole, whole)}\.{_repeat("[0-9]", 1, max_digits - whole)}0*'
2900 for whole in range(max_digits)
2901 ] or [r'0*\.0+']
2902 else:
2903 assert decimal_places is not None
2904 opt_frac_part = rf'\.{_repeat("[0-9]", 0, decimal_places)}0*'
2905 frac_part = rf'\.{_repeat("[0-9]", 1, decimal_places)}0*' if decimal_places else r'\.0+'
2906 alts += [f'[0-9]+(?:{opt_frac_part})?', f'[0-9]*{frac_part}']
2908 alts.append(f'{number}{exponent}')
2909 return rf'^[+-]?(?:{"|".join(alts)}{non_finite})$'
2912def _decimal_serialization_pattern(max_digits: int | None, decimal_places: int | None, allow_inf_nan: bool) -> str:
2913 """Build the decimal pattern for the `'serialization'` mode.
2915 Decimals are serialized using `str()`, producing either:
2917 - The plain notation (`'123.45'`), when the exponent is lower than or equal to 0 and the adjusted
2918 exponent is greater than or equal to -6.
2919 - The scientific notation otherwise (`'1E+3'`, `'1.5E-7'`, `'0E-10'`), with a single digit before
2920 the optional decimal point and an always signed exponent.
2922 Trailing zeros of the decimal part are preserved by `str()`, but aren't counted by the validation logic.
2923 """
2925 def frac_part(places: int) -> str:
2926 return rf'\.{_repeat("[0-9]", 1, places)}0*' if places else r'\.0+'
2928 alts: list[str] = []
2930 # Plain notation:
2931 if max_digits is not None and decimal_places is not None:
2932 whole = max(0, max_digits - decimal_places)
2933 places = min(max_digits, decimal_places)
2934 int_part = f'(?:0|[1-9]{_repeat("[0-9]", 0, whole - 1)})' if whole else '0'
2935 alts.append(f'{int_part}(?:{frac_part(places)})?')
2936 elif max_digits is not None:
2937 # The whole part and decimal part must have at most `max_digits` digits in total,
2938 # so we enumerate the possible widths of the whole part:
2939 alts.append(rf'0(?:\.{_repeat("[0-9]", 1, max_digits)}0*)?')
2940 alts += [
2941 f'[1-9]{_repeat("[0-9]", whole - 1, whole - 1)}(?:{frac_part(max_digits - whole)})?'
2942 for whole in range(1, max_digits + 1)
2943 ]
2944 elif decimal_places is not None:
2945 alts.append(f'(?:0|[1-9][0-9]*)(?:{frac_part(decimal_places)})?')
2946 else:
2947 alts.append(r'(?:0|[1-9][0-9]*)(?:\.[0-9]+)?')
2949 # Zero with an exponent (e.g. `'0E-10'`), which normalizes to zero and thus satisfies any constraint:
2950 alts.append(r'0E[+-][1-9][0-9]*')
2952 # Scientific notation with a positive exponent `k` (e.g. `'1.5E+3'`): the value has `k + 1` whole digits
2953 # and no decimal places:
2954 if max_digits is None:
2955 alts.append(r'[1-9](?:\.[0-9]+)?E\+[1-9][0-9]*')
2956 else:
2957 max_exponent = max_digits - (decimal_places or 0) - 1
2958 if max_exponent >= 1:
2959 alts.append(rf'[1-9](?:\.[0-9]+)?E\+{_int_range_pattern(max_exponent)}')
2961 # Scientific notation with a negative exponent `k` (`k >= 7`, e.g. `'1.5E-7'`): the value has no whole digits,
2962 # and `k` + (the number of significant decimal digits of the coefficient) decimal places:
2963 if max_digits is None and decimal_places is None:
2964 alts.append(r'[1-9](?:\.[0-9]+)?E-[1-9][0-9]*')
2965 else:
2966 max_places = min(c for c in (max_digits, decimal_places) if c is not None)
2967 alts += [f'[1-9](?:{frac_part(max_places - k)})?E-{k}' for k in range(7, max_places + 1)]
2969 if allow_inf_nan:
2970 # `str()` produces `'Infinity'`, `'NaN'` and `'sNaN'` (with an optional payload):
2971 alts += ['Infinity', 's?NaN[0-9]*']
2973 return rf'^-?(?:{"|".join(alts)})$'
2976def _deduplicate_schemas(schemas: Iterable[JsonDict]) -> list[JsonDict]:
2977 return list({_make_json_hashable(schema): schema for schema in schemas}.values())
2980_HashableJsonValue: TypeAlias = (
2981 int | float | str | bool | None | tuple['_HashableJsonValue', ...] | tuple[tuple[str, '_HashableJsonValue'], ...]
2982)
2985def _make_json_hashable(value: JsonValue) -> _HashableJsonValue:
2986 if isinstance(value, dict):
2987 return tuple(sorted((k, _make_json_hashable(v)) for k, v in value.items()))
2988 elif isinstance(value, list):
2989 return tuple(_make_json_hashable(v) for v in value)
2990 else:
2991 return value
2994@dataclasses.dataclass(slots=True)
2995class WithJsonSchema:
2996 """!!! abstract "Usage Documentation"
2997 [`WithJsonSchema` Annotation](../concepts/json_schema.md#withjsonschema-annotation)
2999 An annotation used to override the JSON Schema for a type.
3001 This is useful when you want to set a JSON Schema for a type that don't produce any JSON Schemas by default
3002 (e.g. [`Callable`][collections.abc.Callable]).
3004 If `mode` is set this will only apply to that schema generation mode, allowing you to set different JSON Schemas for validation and serialization.
3006 !!! note
3007 If the `WithJsonSchema` annotation is coupled with the [`Field()`][pydantic.Field] function, the behavior overriding will vary depending on the location:
3009 * If the [`Annotated`][typing.Annotated] metadata is specified at the "top-level" field, `Field()` metadata arguments
3010 (excluding [constraints](../concepts/fields.md#field-constraints)) such as `title` and `description` will be applied on
3011 top of the `WithJsonSchema`, no matter the order:
3013 ```python
3014 from typing import Annotated
3016 from pydantic import BaseModel, Field, WithJsonSchema
3018 class Model(BaseModel):
3019 field: Annotated[
3020 int,
3021 Field(title='My Field'),
3022 WithJsonSchema({'type': 'integer', 'extra': 'data'}),
3023 ]
3025 Model.model_json_schema()['properties']['field']
3026 #> {'type': 'integer', 'extra': 'data', 'title': 'My Field'}
3027 ```
3029 * If the [`Annotated`][typing.Annotated] metadata is specified on a specific inner type, `WithJsonSchema` will unconditionally
3030 override the JSON Schema:
3032 ```python
3033 from typing import Annotated
3035 from pydantic import BaseModel, Field, WithJsonSchema
3037 class Model(BaseModel):
3038 field: list[
3039 Annotated[
3040 int,
3041 Field(title='My Field'),
3042 WithJsonSchema({'type': 'integer', 'extra': 'data'}),
3043 ]
3044 ]
3046 Model.model_json_schema()['properties']['field']
3047 #> {'items': {'extra': 'data', 'type': 'integer'}, 'title': 'Field', 'type': 'array'}
3048 ```
3050 See also the documentation about [the annotated pattern](../concepts/fields.md#the-annotated-pattern).
3051 """
3053 json_schema: JsonSchemaValue | None
3054 mode: Literal['validation', 'serialization'] | None = None
3056 def __get_pydantic_json_schema__(
3057 self, core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler
3058 ) -> JsonSchemaValue:
3059 if self.mode is not None and self.mode != handler.mode:
3060 return handler(core_schema)
3061 if self.json_schema is None:
3062 # This exception is handled in pydantic.json_schema.GenerateJsonSchema._named_required_fields_schema
3063 raise PydanticOmit
3064 else:
3065 return self.json_schema.copy()
3067 def __hash__(self) -> int:
3068 return hash(self.mode)
3071class Examples:
3072 """Add examples to a JSON schema.
3074 If the JSON Schema already contains examples, the provided examples
3075 will be appended.
3077 If `mode` is set this will only apply to that schema generation mode,
3078 allowing you to add different examples for validation and serialization.
3079 """
3081 @overload
3082 @deprecated('Using a dict for `examples` is deprecated since v2.9 and will be removed in v3.0. Use a list instead.')
3083 def __init__(
3084 self, examples: dict[str, Any], mode: Literal['validation', 'serialization'] | None = None
3085 ) -> None: ...
3087 @overload
3088 def __init__(self, examples: list[Any], mode: Literal['validation', 'serialization'] | None = None) -> None: ...
3090 def __init__(
3091 self, examples: dict[str, Any] | list[Any], mode: Literal['validation', 'serialization'] | None = None
3092 ) -> None:
3093 if isinstance(examples, dict):
3094 warnings.warn(
3095 'Using a dict for `examples` is deprecated, use a list instead.',
3096 PydanticDeprecatedSince29,
3097 stacklevel=2,
3098 )
3099 self.examples = examples
3100 self.mode = mode
3102 def __get_pydantic_json_schema__(
3103 self, core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler
3104 ) -> JsonSchemaValue:
3105 mode = self.mode or handler.mode
3106 json_schema = handler(core_schema)
3107 if mode != handler.mode:
3108 return json_schema
3109 examples = json_schema.get('examples')
3110 if examples is None:
3111 json_schema['examples'] = to_jsonable_python(self.examples)
3112 if isinstance(examples, dict):
3113 if isinstance(self.examples, list):
3114 warnings.warn(
3115 'Updating existing JSON Schema examples of type dict with examples of type list. '
3116 'Only the existing examples values will be retained. Note that dict support for '
3117 'examples is deprecated and will be removed in v3.0.',
3118 UserWarning,
3119 )
3120 json_schema['examples'] = to_jsonable_python(
3121 [ex for value in examples.values() for ex in value] + self.examples
3122 )
3123 else:
3124 json_schema['examples'] = to_jsonable_python({**examples, **self.examples})
3125 if isinstance(examples, list):
3126 if isinstance(self.examples, list):
3127 json_schema['examples'] = to_jsonable_python(examples + self.examples)
3128 elif isinstance(self.examples, dict):
3129 warnings.warn(
3130 'Updating existing JSON Schema examples of type list with examples of type dict. '
3131 'Only the examples values will be retained. Note that dict support for '
3132 'examples is deprecated and will be removed in v3.0.',
3133 UserWarning,
3134 )
3135 json_schema['examples'] = to_jsonable_python(
3136 examples + [ex for value in self.examples.values() for ex in value]
3137 )
3139 return json_schema
3141 def __hash__(self) -> int:
3142 return hash(self.mode)
3145def _get_all_json_refs(item: Any) -> set[JsonRef]:
3146 """Get all the definitions references from a JSON schema."""
3147 refs: set[JsonRef] = set()
3148 stack = [item]
3150 while stack:
3151 current = stack.pop()
3152 if isinstance(current, dict):
3153 for key, value in current.items():
3154 if key == 'examples' and isinstance(value, list):
3155 # Skip examples that may contain arbitrary values and references
3156 # (e.g. `{"examples": [{"$ref": "..."}]}`). Note: checking for value
3157 # of type list is necessary to avoid skipping valid portions of the schema,
3158 # for instance when "examples" is used as a property key. A more robust solution
3159 # could be found, but would require more advanced JSON Schema parsing logic.
3160 continue
3161 if key == '$ref' and isinstance(value, str):
3162 refs.add(JsonRef(value))
3163 elif isinstance(value, dict):
3164 stack.append(value)
3165 elif isinstance(value, list):
3166 stack.extend(value)
3167 elif isinstance(current, list):
3168 stack.extend(current)
3170 return refs
3173AnyType = TypeVar('AnyType')
3175if TYPE_CHECKING:
3176 SkipJsonSchema = Annotated[AnyType, ...]
3177else:
3179 @dataclasses.dataclass(slots=True)
3180 class SkipJsonSchema:
3181 """!!! abstract "Usage Documentation"
3182 [`SkipJsonSchema` Annotation](../concepts/json_schema.md#skipjsonschema-annotation)
3184 Add this as an annotation on a field to skip generating a JSON schema for that field.
3186 Example:
3187 ```python
3188 from pprint import pprint
3190 from pydantic import BaseModel
3191 from pydantic.json_schema import SkipJsonSchema
3193 class Model(BaseModel):
3194 a: int | None = None # (1)!
3195 b: int | SkipJsonSchema[None] = None # (2)!
3196 c: SkipJsonSchema[int | None] = None # (3)!
3198 pprint(Model.model_json_schema())
3199 '''
3200 {
3201 'properties': {
3202 'a': {
3203 'anyOf': [
3204 {'type': 'integer'},
3205 {'type': 'null'}
3206 ],
3207 'default': None,
3208 'title': 'A'
3209 },
3210 'b': {
3211 'default': None,
3212 'title': 'B',
3213 'type': 'integer'
3214 }
3215 },
3216 'title': 'Model',
3217 'type': 'object'
3218 }
3219 '''
3220 ```
3222 1. The integer and null types are both included in the schema for `a`.
3223 2. The integer type is the only type included in the schema for `b`.
3224 3. The entirety of the `c` field is omitted from the schema.
3225 """
3227 def __class_getitem__(cls, item: AnyType) -> AnyType:
3228 return Annotated[item, cls()]
3230 def __get_pydantic_json_schema__(
3231 self, core_schema: CoreSchema, handler: GetJsonSchemaHandler
3232 ) -> JsonSchemaValue:
3233 raise PydanticOmit
3235 def __hash__(self) -> int:
3236 return hash(type(self))
3239def _get_typed_dict_config(cls: type[Any] | None) -> ConfigDict | None:
3240 # If typed dictionary doesn't have config, the parent's config is used (as during core schema generation),
3241 # hence a default of `None`:
3242 if cls is not None:
3243 try:
3244 return _decorators.get_attribute_from_bases(cls, '__pydantic_config__')
3245 except AttributeError:
3246 pass
3247 return None
3250def _get_ser_schema_for_default_value(schema: CoreSchema) -> core_schema.PlainSerializerFunctionSerSchema | None:
3251 """Get a `'function-plain'` serialization schema that can be used to serialize a default value.
3253 This takes into account having the serialization schema nested under validation schema(s).
3254 """
3255 if (
3256 (ser_schema := schema.get('serialization'))
3257 and ser_schema['type'] == 'function-plain'
3258 and not ser_schema.get('info_arg')
3259 ):
3260 return cast('core_schema.PlainSerializerFunctionSerSchema', ser_schema)
3261 if _core_utils.is_function_with_inner_schema(schema):
3262 return _get_ser_schema_for_default_value(schema['schema'])
3265def _is_nullable_schema(schema: CoreSchemaOrField) -> bool:
3266 """Whether the schema is a `'nullable'` schema, possibly wrapped in default, validator function, chain,
3267 lax-or-strict and JSON-or-Python schemas.
3269 For the latter three, the same inner schema as the one used to generate the serialization JSON Schema
3270 is checked (see the corresponding `GenerateJsonSchema` methods).
3271 """
3272 if schema['type'] == 'default' or _core_utils.is_function_with_inner_schema(schema):
3273 return _is_nullable_schema(schema['schema'])
3274 if schema['type'] == 'chain':
3275 return _is_nullable_schema(schema['steps'][-1])
3276 if schema['type'] == 'lax-or-strict':
3277 # See TODO in `GenerateJsonSchema.lax_or_strict_schema()`:
3278 return _is_nullable_schema(schema['strict_schema'] if schema.get('strict', False) else schema['lax_schema'])
3279 if schema['type'] == 'json-or-python':
3280 return _is_nullable_schema(schema['json_schema'])
3281 return schema['type'] == 'nullable'