Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pydantic/json_schema.py: 17%
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 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 Callable,
31 Literal,
32 NewType,
33 TypeVar,
34 Union,
35 cast,
36 overload,
37)
39import pydantic_core
40from pydantic_core import MISSING, CoreSchema, PydanticOmit, core_schema, to_jsonable_python
41from pydantic_core.core_schema import ComputedField
42from typing_extensions import TypeAlias, assert_never, deprecated, final
43from typing_inspection.introspection import get_literal_values
45from pydantic.warnings import PydanticDeprecatedSince26, PydanticDeprecatedSince29
47from ._internal import (
48 _config,
49 _core_metadata,
50 _core_utils,
51 _decorators,
52 _internal_dataclass,
53 _mock_val_ser,
54 _schema_generation_shared,
55 _typing_extra,
56)
57from .annotated_handlers import GetJsonSchemaHandler
58from .config import JsonDict, JsonValue
59from .errors import PydanticInvalidForJsonSchema, PydanticSchemaGenerationError, PydanticUserError
61if TYPE_CHECKING:
62 from . import ConfigDict
63 from ._internal._core_utils import CoreSchemaField, CoreSchemaOrField
64 from ._internal._dataclasses import PydanticDataclass
65 from ._internal._schema_generation_shared import GetJsonSchemaFunction
66 from .main import BaseModel
69CoreSchemaOrFieldType = Literal[core_schema.CoreSchemaType, core_schema.CoreSchemaFieldType]
70"""
71A type alias for defined schema types that represents a union of
72`core_schema.CoreSchemaType` and
73`core_schema.CoreSchemaFieldType`.
74"""
76JsonSchemaValue = dict[str, Any]
77"""
78A type alias for a JSON schema value. This is a dictionary of string keys to arbitrary JSON values.
79"""
81JsonSchemaMode = Literal['validation', 'serialization']
82"""
83A type alias that represents the mode of a JSON schema; either 'validation' or 'serialization'.
85For some types, the inputs to validation differ from the outputs of serialization. For example,
86computed fields will only be present when serializing, and should not be provided when
87validating. This flag provides a way to indicate whether you want the JSON schema required
88for validation inputs, or that will be matched by serialization outputs.
89"""
91_MODE_TITLE_MAPPING: dict[JsonSchemaMode, str] = {'validation': 'Input', 'serialization': 'Output'}
94JsonSchemaWarningKind = Literal['skipped-choice', 'non-serializable-default', 'skipped-discriminator']
95"""
96A type alias representing the kinds of warnings that can be emitted during JSON schema generation.
98See [`GenerateJsonSchema.render_warning_message`][pydantic.json_schema.GenerateJsonSchema.render_warning_message]
99for more details.
100"""
103class PydanticJsonSchemaWarning(UserWarning):
104 """This class is used to emit warnings produced during JSON schema generation.
105 See the [`GenerateJsonSchema.emit_warning`][pydantic.json_schema.GenerateJsonSchema.emit_warning] and
106 [`GenerateJsonSchema.render_warning_message`][pydantic.json_schema.GenerateJsonSchema.render_warning_message]
107 methods for more details; these can be overridden to control warning behavior.
108 """
111NoDefault = object()
112"""A sentinel value used to indicate that no default value should be used when generating a JSON Schema
113for a core schema with a default value.
114"""
117# ##### JSON Schema Generation #####
118DEFAULT_REF_TEMPLATE = '#/$defs/{model}'
119"""The default format string used to generate reference names."""
121# There are three types of references relevant to building JSON schemas:
122# 1. core_schema "ref" values; these are not exposed as part of the JSON schema
123# * these might look like the fully qualified path of a model, its id, or something similar
124CoreRef = NewType('CoreRef', str)
125# 2. keys of the "definitions" object that will eventually go into the JSON schema
126# * by default, these look like "MyModel", though may change in the presence of collisions
127# * eventually, we may want to make it easier to modify the way these names are generated
128DefsRef = NewType('DefsRef', str)
129# 3. the values corresponding to the "$ref" key in the schema
130# * By default, these look like "#/$defs/MyModel", as in {"$ref": "#/$defs/MyModel"}
131JsonRef = NewType('JsonRef', str)
133CoreModeRef = tuple[CoreRef, JsonSchemaMode]
134JsonSchemaKeyT = TypeVar('JsonSchemaKeyT', bound=Hashable)
136_PRIMITIVE_JSON_SCHEMA_TYPES = ('string', 'boolean', 'null', 'integer', 'number')
139@dataclasses.dataclass(**_internal_dataclass.slots_true)
140class _DefinitionsRemapping:
141 defs_remapping: dict[DefsRef, DefsRef]
142 json_remapping: dict[JsonRef, JsonRef]
144 @staticmethod
145 def from_prioritized_choices(
146 prioritized_choices: dict[DefsRef, list[DefsRef]],
147 defs_to_json: dict[DefsRef, JsonRef],
148 definitions: dict[DefsRef, JsonSchemaValue],
149 ) -> _DefinitionsRemapping:
150 """
151 This function should produce a remapping that replaces complex DefsRef with the simpler ones from the
152 prioritized_choices such that applying the name remapping would result in an equivalent JSON schema.
153 """
154 # We need to iteratively simplify the definitions until we reach a fixed point.
155 # The reason for this is that outer definitions may reference inner definitions that get simplified
156 # into an equivalent reference, and the outer definitions won't be equivalent until we've simplified
157 # the inner definitions.
158 copied_definitions = deepcopy(definitions)
159 definitions_schema = {'$defs': copied_definitions}
160 for _iter in range(100): # prevent an infinite loop in the case of a bug, 100 iterations should be enough
161 # For every possible remapped DefsRef, collect all schemas that DefsRef might be used for:
162 schemas_for_alternatives: dict[DefsRef, list[JsonSchemaValue]] = defaultdict(list)
163 for defs_ref in copied_definitions:
164 alternatives = prioritized_choices[defs_ref]
165 for alternative in alternatives:
166 schemas_for_alternatives[alternative].append(copied_definitions[defs_ref])
168 # Deduplicate the schemas for each alternative; the idea is that we only want to remap to a new DefsRef
169 # if it introduces no ambiguity, i.e., there is only one distinct schema for that DefsRef.
170 for defs_ref in schemas_for_alternatives:
171 schemas_for_alternatives[defs_ref] = _deduplicate_schemas(schemas_for_alternatives[defs_ref])
173 # Build the remapping
174 defs_remapping: dict[DefsRef, DefsRef] = {}
175 json_remapping: dict[JsonRef, JsonRef] = {}
176 for original_defs_ref in definitions:
177 alternatives = prioritized_choices[original_defs_ref]
178 # Pick the first alternative that has only one schema, since that means there is no collision
179 remapped_defs_ref = next(x for x in alternatives if len(schemas_for_alternatives[x]) == 1)
180 defs_remapping[original_defs_ref] = remapped_defs_ref
182 # Map all alternatives after the remapped one to the remapped one
183 # This ensures that intermediate simplifications are also remapped
184 remapped_index = alternatives.index(remapped_defs_ref)
185 for alt in alternatives[remapped_index:]:
186 json_remapping[defs_to_json[alt]] = defs_to_json[remapped_defs_ref]
187 remapping = _DefinitionsRemapping(defs_remapping, json_remapping)
188 new_definitions_schema = remapping.remap_json_schema({'$defs': copied_definitions})
189 if definitions_schema == new_definitions_schema:
190 # We've reached the fixed point
191 return remapping
192 definitions_schema = new_definitions_schema
194 raise PydanticInvalidForJsonSchema('Failed to simplify the JSON schema definitions')
196 def remap_defs_ref(self, ref: DefsRef) -> DefsRef:
197 return self.defs_remapping.get(ref, ref)
199 def remap_json_ref(self, ref: JsonRef) -> JsonRef:
200 return self.json_remapping.get(ref, ref)
202 def remap_json_schema(self, schema: Any) -> Any:
203 """
204 Recursively update the JSON schema replacing all $refs
205 """
206 if isinstance(schema, str):
207 # Note: this may not really be a JsonRef; we rely on having no collisions between JsonRefs and other strings
208 return self.remap_json_ref(JsonRef(schema))
209 elif isinstance(schema, list):
210 return [self.remap_json_schema(item) for item in schema]
211 elif isinstance(schema, dict):
212 for key, value in schema.items():
213 if key == '$ref' and isinstance(value, str):
214 schema['$ref'] = self.remap_json_ref(JsonRef(value))
215 elif key == '$defs':
216 schema['$defs'] = {
217 self.remap_defs_ref(DefsRef(key)): self.remap_json_schema(value)
218 for key, value in schema['$defs'].items()
219 }
220 else:
221 schema[key] = self.remap_json_schema(value)
222 return schema
225class GenerateJsonSchema:
226 """!!! abstract "Usage Documentation"
227 [Customizing the JSON Schema Generation Process](../concepts/json_schema.md#customizing-the-json-schema-generation-process)
229 A class for generating JSON schemas.
231 This class generates JSON schemas based on configured parameters. The default schema dialect
232 is [https://json-schema.org/draft/2020-12/schema](https://json-schema.org/draft/2020-12/schema).
233 The class uses `by_alias` to configure how fields with
234 multiple names are handled and `ref_template` to format reference names.
236 Attributes:
237 schema_dialect: The JSON schema dialect used to generate the schema. See
238 [Declaring a Dialect](https://json-schema.org/understanding-json-schema/reference/schema.html#id4)
239 in the JSON Schema documentation for more information about dialects.
240 ignored_warning_kinds: Warnings to ignore when generating the schema. `self.render_warning_message` will
241 do nothing if its argument `kind` is in `ignored_warning_kinds`;
242 this value can be modified on subclasses to easily control which warnings are emitted.
243 by_alias: Whether to use field aliases when generating the schema.
244 ref_template: The format string used when generating reference names.
245 core_to_json_refs: A mapping of core refs to JSON refs.
246 core_to_defs_refs: A mapping of core refs to definition refs.
247 defs_to_core_refs: A mapping of definition refs to core refs.
248 json_to_defs_refs: A mapping of JSON refs to definition refs.
249 definitions: Definitions in the schema.
251 Args:
252 by_alias: Whether to use field aliases in the generated schemas.
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 = True,
276 ref_template: str = DEFAULT_REF_TEMPLATE,
277 union_format: Literal['any_of', 'primitive_type_array'] = 'any_of',
278 ) -> None:
279 self.by_alias = by_alias
280 self.ref_template = ref_template
281 self.union_format: Literal['any_of', 'primitive_type_array'] = union_format
283 self.core_to_json_refs: dict[CoreModeRef, JsonRef] = {}
284 self.core_to_defs_refs: dict[CoreModeRef, DefsRef] = {}
285 self.defs_to_core_refs: dict[DefsRef, CoreModeRef] = {}
286 self.json_to_defs_refs: dict[JsonRef, DefsRef] = {}
288 self.definitions: dict[DefsRef, JsonSchemaValue] = {}
289 self._config_wrapper_stack = _config.ConfigWrapperStack(_config.ConfigWrapper({}))
291 self._mode: JsonSchemaMode = 'validation'
293 # The following includes a mapping of a fully-unique defs ref choice to a list of preferred
294 # alternatives, which are generally simpler, such as only including the class name.
295 # At the end of schema generation, we use these to produce a JSON schema with more human-readable
296 # definitions, which would also work better in a generated OpenAPI client, etc.
297 self._prioritized_defsref_choices: dict[DefsRef, list[DefsRef]] = {}
298 self._collision_counter: dict[str, int] = defaultdict(int)
299 self._collision_index: dict[str, int] = {}
301 self._schema_type_to_method = self.build_schema_type_to_method()
303 # When we encounter definitions we need to try to build them immediately
304 # so that they are available schemas that reference them
305 # But it's possible that CoreSchema was never going to be used
306 # (e.g. because the CoreSchema that references short circuits is JSON schema generation without needing
307 # the reference) so instead of failing altogether if we can't build a definition we
308 # store the error raised and re-throw it if we end up needing that def
309 self._core_defs_invalid_for_json_schema: dict[DefsRef, PydanticInvalidForJsonSchema] = {}
311 # This changes to True after generating a schema, to prevent issues caused by accidental reuse
312 # of a single instance of a schema generator
313 self._used = False
315 @property
316 def _config(self) -> _config.ConfigWrapper:
317 return self._config_wrapper_stack.tail
319 @property
320 def mode(self) -> JsonSchemaMode:
321 if self._config.json_schema_mode_override is not None:
322 return self._config.json_schema_mode_override
323 else:
324 return self._mode
326 def build_schema_type_to_method(
327 self,
328 ) -> dict[CoreSchemaOrFieldType, Callable[[CoreSchemaOrField], JsonSchemaValue]]:
329 """Builds a dictionary mapping fields to methods for generating JSON schemas.
331 Returns:
332 A dictionary containing the mapping of `CoreSchemaOrFieldType` to a handler method.
334 Raises:
335 TypeError: If no method has been defined for generating a JSON schema for a given pydantic core schema type.
336 """
337 mapping: dict[CoreSchemaOrFieldType, Callable[[CoreSchemaOrField], JsonSchemaValue]] = {}
338 core_schema_types: list[CoreSchemaOrFieldType] = list(get_literal_values(CoreSchemaOrFieldType))
339 for key in core_schema_types:
340 method_name = f'{key.replace("-", "_")}_schema'
341 try:
342 mapping[key] = getattr(self, method_name)
343 except AttributeError as e: # pragma: no cover
344 if os.getenv('PYDANTIC_PRIVATE_ALLOW_UNHANDLED_SCHEMA_TYPES'):
345 continue
346 raise TypeError(
347 f'No method for generating JsonSchema for core_schema.type={key!r} '
348 f'(expected: {type(self).__name__}.{method_name})'
349 ) from e
350 return mapping
352 def generate_definitions(
353 self, inputs: Sequence[tuple[JsonSchemaKeyT, JsonSchemaMode, core_schema.CoreSchema]]
354 ) -> tuple[dict[tuple[JsonSchemaKeyT, JsonSchemaMode], JsonSchemaValue], dict[DefsRef, JsonSchemaValue]]:
355 """Generates JSON schema definitions from a list of core schemas, pairing the generated definitions with a
356 mapping that links the input keys to the definition references.
358 Args:
359 inputs: A sequence of tuples, where:
361 - The first element is a JSON schema key type.
362 - The second element is the JSON mode: either 'validation' or 'serialization'.
363 - The third element is a core schema.
365 Returns:
366 A tuple where:
368 - The first element is a dictionary whose keys are tuples of JSON schema key type and JSON mode, and
369 whose values are the JSON schema corresponding to that pair of inputs. (These schemas may have
370 JsonRef references to definitions that are defined in the second returned element.)
371 - The second element is a dictionary whose keys are definition references for the JSON schemas
372 from the first returned element, and whose values are the actual JSON schema definitions.
374 Raises:
375 PydanticUserError: Raised if the JSON schema generator has already been used to generate a JSON schema.
376 """
377 if self._used:
378 raise PydanticUserError(
379 'This JSON schema generator has already been used to generate a JSON schema. '
380 f'You must create a new instance of {type(self).__name__} to generate a new JSON schema.',
381 code='json-schema-already-used',
382 )
384 for _, mode, schema in inputs:
385 self._mode = mode
386 self.generate_inner(schema)
388 definitions_remapping = self._build_definitions_remapping()
390 json_schemas_map: dict[tuple[JsonSchemaKeyT, JsonSchemaMode], DefsRef] = {}
391 for key, mode, schema in inputs:
392 self._mode = mode
393 json_schema = self.generate_inner(schema)
394 json_schemas_map[(key, mode)] = definitions_remapping.remap_json_schema(json_schema)
396 json_schema = {'$defs': self.definitions}
397 json_schema = definitions_remapping.remap_json_schema(json_schema)
398 self._used = True
399 return json_schemas_map, self.sort(json_schema['$defs']) # type: ignore
401 def generate(self, schema: CoreSchema, mode: JsonSchemaMode = 'validation') -> JsonSchemaValue:
402 """Generates a JSON schema for a specified schema in a specified mode.
404 Args:
405 schema: A Pydantic model.
406 mode: The mode in which to generate the schema. Defaults to 'validation'.
408 Returns:
409 A JSON schema representing the specified schema.
411 Raises:
412 PydanticUserError: If the JSON schema generator has already been used to generate a JSON schema.
413 """
414 self._mode = mode
415 if self._used:
416 raise PydanticUserError(
417 'This JSON schema generator has already been used to generate a JSON schema. '
418 f'You must create a new instance of {type(self).__name__} to generate a new JSON schema.',
419 code='json-schema-already-used',
420 )
422 json_schema: JsonSchemaValue = self.generate_inner(schema)
423 json_ref_counts = self.get_json_ref_counts(json_schema)
425 ref = cast(JsonRef, json_schema.get('$ref'))
426 while ref is not None: # may need to unpack multiple levels
427 ref_json_schema = self.get_schema_from_definitions(ref)
428 if json_ref_counts[ref] == 1 and ref_json_schema is not None and len(json_schema) == 1:
429 # "Unpack" the ref since this is the only reference and there are no sibling keys
430 json_schema = ref_json_schema.copy() # copy to prevent recursive dict reference
431 json_ref_counts[ref] -= 1
432 ref = cast(JsonRef, json_schema.get('$ref'))
433 ref = None
435 self._garbage_collect_definitions(json_schema)
436 definitions_remapping = self._build_definitions_remapping()
438 if self.definitions:
439 json_schema['$defs'] = self.definitions
441 json_schema = definitions_remapping.remap_json_schema(json_schema)
443 # For now, we will not set the $schema key. However, if desired, this can be easily added by overriding
444 # this method and adding the following line after a call to super().generate(schema):
445 # json_schema['$schema'] = self.schema_dialect
447 self._used = True
448 return self.sort(json_schema)
450 def generate_inner(self, schema: CoreSchemaOrField) -> JsonSchemaValue: # noqa: C901
451 """Generates a JSON schema for a given core schema.
453 Args:
454 schema: The given core schema.
456 Returns:
457 The generated JSON schema.
459 TODO: the nested function definitions here seem like bad practice, I'd like to unpack these
460 in a future PR. It'd be great if we could shorten the call stack a bit for JSON schema generation,
461 and I think there's potential for that here.
462 """
463 # If a schema with the same CoreRef has been handled, just return a reference to it
464 # Note that this assumes that it will _never_ be the case that the same CoreRef is used
465 # on types that should have different JSON schemas
466 if 'ref' in schema:
467 core_ref = CoreRef(schema['ref']) # type: ignore[typeddict-item]
468 core_mode_ref = (core_ref, self.mode)
469 if core_mode_ref in self.core_to_defs_refs and self.core_to_defs_refs[core_mode_ref] in self.definitions:
470 return {'$ref': self.core_to_json_refs[core_mode_ref]}
472 def populate_defs(core_schema: CoreSchema, json_schema: JsonSchemaValue) -> JsonSchemaValue:
473 if 'ref' in core_schema:
474 core_ref = CoreRef(core_schema['ref']) # type: ignore[typeddict-item]
475 defs_ref, ref_json_schema = self.get_cache_defs_ref_schema(core_ref)
476 json_ref = JsonRef(ref_json_schema['$ref'])
477 # Replace the schema if it's not a reference to itself
478 # What we want to avoid is having the def be just a ref to itself
479 # which is what would happen if we blindly assigned any
480 if json_schema.get('$ref', None) != json_ref:
481 self.definitions[defs_ref] = json_schema
482 self._core_defs_invalid_for_json_schema.pop(defs_ref, None)
483 json_schema = ref_json_schema
484 return json_schema
486 def handler_func(schema_or_field: CoreSchemaOrField) -> JsonSchemaValue:
487 """Generate a JSON schema based on the input schema.
489 Args:
490 schema_or_field: The core schema to generate a JSON schema from.
492 Returns:
493 The generated JSON schema.
495 Raises:
496 TypeError: If an unexpected schema type is encountered.
497 """
498 # Generate the core-schema-type-specific bits of the schema generation:
499 json_schema: JsonSchemaValue | None = None
500 if self.mode == 'serialization' and 'serialization' in schema_or_field:
501 # In this case, we skip the JSON Schema generation of the schema
502 # and use the `'serialization'` schema instead (canonical example:
503 # `Annotated[int, PlainSerializer(str)]`).
504 ser_schema = schema_or_field['serialization'] # type: ignore
505 json_schema = self.ser_schema(ser_schema)
507 # It might be that the 'serialization'` is skipped depending on `when_used`.
508 # This is only relevant for `nullable` schemas though, so we special case here.
509 if (
510 json_schema is not None
511 and ser_schema.get('when_used') in ('unless-none', 'json-unless-none')
512 and schema_or_field['type'] == 'nullable'
513 ):
514 json_schema = self.get_union_of_schemas([{'type': 'null'}, json_schema])
515 if json_schema is None:
516 if _core_utils.is_core_schema(schema_or_field) or _core_utils.is_core_schema_field(schema_or_field):
517 generate_for_schema_type = self._schema_type_to_method[schema_or_field['type']]
518 json_schema = generate_for_schema_type(schema_or_field)
519 else:
520 raise TypeError(f'Unexpected schema type: schema={schema_or_field}')
521 return json_schema
523 current_handler = _schema_generation_shared.GenerateJsonSchemaHandler(self, handler_func)
525 metadata = cast(_core_metadata.CoreMetadata, schema.get('metadata', {}))
527 # TODO: I dislike that we have to wrap these basic dict updates in callables, is there any way around this?
529 if js_updates := metadata.get('pydantic_js_updates'):
531 def js_updates_handler_func(
532 schema_or_field: CoreSchemaOrField,
533 current_handler: GetJsonSchemaHandler = current_handler,
534 ) -> JsonSchemaValue:
535 json_schema = {**current_handler(schema_or_field), **js_updates}
536 return json_schema
538 current_handler = _schema_generation_shared.GenerateJsonSchemaHandler(self, js_updates_handler_func)
540 if js_extra := metadata.get('pydantic_js_extra'):
542 def js_extra_handler_func(
543 schema_or_field: CoreSchemaOrField,
544 current_handler: GetJsonSchemaHandler = current_handler,
545 ) -> JsonSchemaValue:
546 json_schema = current_handler(schema_or_field)
547 if isinstance(js_extra, dict):
548 json_schema.update(to_jsonable_python(js_extra))
549 elif callable(js_extra):
550 # similar to typing issue in _update_class_schema when we're working with callable js extra
551 js_extra(json_schema) # type: ignore
552 return json_schema
554 current_handler = _schema_generation_shared.GenerateJsonSchemaHandler(self, js_extra_handler_func)
556 for js_modify_function in metadata.get('pydantic_js_functions', ()):
558 def new_handler_func(
559 schema_or_field: CoreSchemaOrField,
560 current_handler: GetJsonSchemaHandler = current_handler,
561 js_modify_function: GetJsonSchemaFunction = js_modify_function,
562 ) -> JsonSchemaValue:
563 json_schema = js_modify_function(schema_or_field, current_handler)
564 if _core_utils.is_core_schema(schema_or_field):
565 json_schema = populate_defs(schema_or_field, json_schema)
566 original_schema = current_handler.resolve_ref_schema(json_schema)
567 ref = json_schema.pop('$ref', None)
568 if ref and json_schema:
569 original_schema.update(json_schema)
570 return original_schema
572 current_handler = _schema_generation_shared.GenerateJsonSchemaHandler(self, new_handler_func)
574 for js_modify_function in metadata.get('pydantic_js_annotation_functions', ()):
576 def new_handler_func(
577 schema_or_field: CoreSchemaOrField,
578 current_handler: GetJsonSchemaHandler = current_handler,
579 js_modify_function: GetJsonSchemaFunction = js_modify_function,
580 ) -> JsonSchemaValue:
581 return js_modify_function(schema_or_field, current_handler)
583 current_handler = _schema_generation_shared.GenerateJsonSchemaHandler(self, new_handler_func)
585 json_schema = current_handler(schema)
586 if _core_utils.is_core_schema(schema):
587 json_schema = populate_defs(schema, json_schema)
588 return json_schema
590 def sort(self, value: JsonSchemaValue, parent_key: str | None = None) -> JsonSchemaValue:
591 """Override this method to customize the sorting of the JSON schema (e.g., don't sort at all, sort all keys unconditionally, etc.)
593 By default, alphabetically sort the keys in the JSON schema, skipping the 'properties' and 'default' keys to preserve field definition order.
594 This sort is recursive, so it will sort all nested dictionaries as well.
595 """
596 sorted_dict: dict[str, JsonSchemaValue] = {}
597 keys = value.keys()
598 if parent_key not in ('properties', 'default'):
599 keys = sorted(keys)
600 for key in keys:
601 sorted_dict[key] = self._sort_recursive(value[key], parent_key=key)
602 return sorted_dict
604 def _sort_recursive(self, value: Any, parent_key: str | None = None) -> Any:
605 """Recursively sort a JSON schema value."""
606 if isinstance(value, dict):
607 sorted_dict: dict[str, JsonSchemaValue] = {}
608 keys = value.keys()
609 if parent_key not in ('properties', 'default'):
610 keys = sorted(keys)
611 for key in keys:
612 sorted_dict[key] = self._sort_recursive(value[key], parent_key=key)
613 return sorted_dict
614 elif isinstance(value, list):
615 sorted_list: list[JsonSchemaValue] = [self._sort_recursive(item, parent_key) for item in value]
616 return sorted_list
617 else:
618 return value
620 # ### Schema generation methods
622 def invalid_schema(self, schema: core_schema.InvalidSchema) -> JsonSchemaValue:
623 """Placeholder - should never be called."""
625 raise RuntimeError('Cannot generate schema for invalid_schema. This is a bug! Please report it.')
627 def any_schema(self, schema: core_schema.AnySchema) -> JsonSchemaValue:
628 """Generates a JSON schema that matches any value.
630 Args:
631 schema: The core schema.
633 Returns:
634 The generated JSON schema.
635 """
636 return {}
638 def none_schema(self, schema: core_schema.NoneSchema) -> JsonSchemaValue:
639 """Generates a JSON schema that matches `None`.
641 Args:
642 schema: The core schema.
644 Returns:
645 The generated JSON schema.
646 """
647 return {'type': 'null'}
649 def bool_schema(self, schema: core_schema.BoolSchema) -> JsonSchemaValue:
650 """Generates a JSON schema that matches a bool value.
652 Args:
653 schema: The core schema.
655 Returns:
656 The generated JSON schema.
657 """
658 return {'type': 'boolean'}
660 def int_schema(self, schema: core_schema.IntSchema) -> JsonSchemaValue:
661 """Generates a JSON schema that matches an int value.
663 Args:
664 schema: The core schema.
666 Returns:
667 The generated JSON schema.
668 """
669 json_schema: dict[str, Any] = {'type': 'integer'}
670 self.update_with_validations(json_schema, schema, self.ValidationsMapping.numeric)
671 json_schema = {k: v for k, v in json_schema.items() if v not in {math.inf, -math.inf}}
672 return json_schema
674 def float_schema(self, schema: core_schema.FloatSchema) -> JsonSchemaValue:
675 """Generates a JSON schema that matches a float value.
677 Args:
678 schema: The core schema.
680 Returns:
681 The generated JSON schema.
682 """
683 json_schema: dict[str, Any] = {'type': 'number'}
684 self.update_with_validations(json_schema, schema, self.ValidationsMapping.numeric)
685 json_schema = {k: v for k, v in json_schema.items() if v not in {math.inf, -math.inf}}
686 return json_schema
688 def decimal_schema(self, schema: core_schema.DecimalSchema) -> JsonSchemaValue:
689 """Generates a JSON schema that matches a decimal value.
691 Args:
692 schema: The core schema.
694 Returns:
695 The generated JSON schema.
696 """
698 def get_decimal_pattern(schema: core_schema.DecimalSchema) -> str:
699 max_digits = schema.get('max_digits')
700 decimal_places = schema.get('decimal_places')
702 pattern = (
703 r'^(?!^[-+.]*$)[+-]?0*' # check it is not empty string and not one or sequence of ".+-" characters.
704 )
706 # Case 1: Both max_digits and decimal_places are set
707 if max_digits is not None and decimal_places is not None:
708 integer_places = max(0, max_digits - decimal_places)
709 pattern += (
710 rf'(?:'
711 rf'\d{{0,{integer_places}}}'
712 rf'|'
713 rf'(?=[\d.]{{1,{max_digits + 1}}}0*$)'
714 rf'\d{{0,{integer_places}}}\.\d{{0,{decimal_places}}}0*$'
715 rf')'
716 )
718 # Case 2: Only max_digits is set
719 elif max_digits is not None and decimal_places is None:
720 pattern += (
721 rf'(?:'
722 rf'\d{{0,{max_digits}}}'
723 rf'|'
724 rf'(?=[\d.]{{1,{max_digits + 1}}}0*$)'
725 rf'\d*\.\d*0*$'
726 rf')'
727 )
729 # Case 3: Only decimal_places is set
730 elif max_digits is None and decimal_places is not None:
731 pattern += rf'\d*\.?\d{{0,{decimal_places}}}0*$'
733 # Case 4: Both are None (no restrictions)
734 else:
735 pattern += r'\d*\.?\d*$' # look for arbitrary integer or decimal
737 return pattern
739 json_schema = self.str_schema(core_schema.str_schema(pattern=get_decimal_pattern(schema)))
740 if self.mode == 'validation':
741 multiple_of = schema.get('multiple_of')
742 le = schema.get('le')
743 ge = schema.get('ge')
744 lt = schema.get('lt')
745 gt = schema.get('gt')
746 json_schema = {
747 'anyOf': [
748 self.float_schema(
749 core_schema.float_schema(
750 allow_inf_nan=schema.get('allow_inf_nan'),
751 multiple_of=None if multiple_of is None else float(multiple_of),
752 le=None if le is None else float(le),
753 ge=None if ge is None else float(ge),
754 lt=None if lt is None else float(lt),
755 gt=None if gt is None else float(gt),
756 )
757 ),
758 json_schema,
759 ],
760 }
761 return json_schema
763 def str_schema(self, schema: core_schema.StringSchema) -> JsonSchemaValue:
764 """Generates a JSON schema that matches a string value.
766 Args:
767 schema: The core schema.
769 Returns:
770 The generated JSON schema.
771 """
772 json_schema = {'type': 'string'}
773 self.update_with_validations(json_schema, schema, self.ValidationsMapping.string)
774 if isinstance(json_schema.get('pattern'), Pattern):
775 # TODO: should we add regex flags to the pattern?
776 json_schema['pattern'] = json_schema.get('pattern').pattern # type: ignore
777 return json_schema
779 def bytes_schema(self, schema: core_schema.BytesSchema) -> JsonSchemaValue:
780 """Generates a JSON schema that matches a bytes value.
782 Args:
783 schema: The core schema.
785 Returns:
786 The generated JSON schema.
787 """
788 json_schema = {'type': 'string', 'format': 'base64url' if self._config.ser_json_bytes == 'base64' else 'binary'}
789 self.update_with_validations(json_schema, schema, self.ValidationsMapping.bytes)
790 return json_schema
792 def date_schema(self, schema: core_schema.DateSchema) -> JsonSchemaValue:
793 """Generates a JSON schema that matches a date value.
795 Args:
796 schema: The core schema.
798 Returns:
799 The generated JSON schema.
800 """
801 return {'type': 'string', 'format': 'date'}
803 def time_schema(self, schema: core_schema.TimeSchema) -> JsonSchemaValue:
804 """Generates a JSON schema that matches a time value.
806 Args:
807 schema: The core schema.
809 Returns:
810 The generated JSON schema.
811 """
812 return {'type': 'string', 'format': 'time'}
814 def datetime_schema(self, schema: core_schema.DatetimeSchema) -> JsonSchemaValue:
815 """Generates a JSON schema that matches a datetime value.
817 Args:
818 schema: The core schema.
820 Returns:
821 The generated JSON schema.
822 """
823 return {'type': 'string', 'format': 'date-time'}
825 def timedelta_schema(self, schema: core_schema.TimedeltaSchema) -> JsonSchemaValue:
826 """Generates a JSON schema that matches a timedelta value.
828 Args:
829 schema: The core schema.
831 Returns:
832 The generated JSON schema.
833 """
834 if self._config.ser_json_timedelta == 'float':
835 return {'type': 'number'}
836 return {'type': 'string', 'format': 'duration'}
838 def literal_schema(self, schema: core_schema.LiteralSchema) -> JsonSchemaValue:
839 """Generates a JSON schema that matches a literal value.
841 Args:
842 schema: The core schema.
844 Returns:
845 The generated JSON schema.
846 """
847 expected = [to_jsonable_python(v.value if isinstance(v, Enum) else v) for v in schema['expected']]
849 result: dict[str, Any] = {}
850 if len(expected) == 1:
851 result['const'] = expected[0]
852 else:
853 result['enum'] = expected
855 types = {type(e) for e in expected}
856 if types == {str}:
857 result['type'] = 'string'
858 elif types == {int}:
859 result['type'] = 'integer'
860 elif types == {float}:
861 result['type'] = 'number'
862 elif types == {bool}:
863 result['type'] = 'boolean'
864 elif types == {list}:
865 result['type'] = 'array'
866 elif types == {type(None)}:
867 result['type'] = 'null'
868 return result
870 def missing_sentinel_schema(self, schema: core_schema.MissingSentinelSchema) -> JsonSchemaValue:
871 """Generates a JSON schema that matches the `MISSING` sentinel value.
873 Args:
874 schema: The core schema.
876 Returns:
877 The generated JSON schema.
878 """
879 raise PydanticOmit
881 def enum_schema(self, schema: core_schema.EnumSchema) -> JsonSchemaValue:
882 """Generates a JSON schema that matches an Enum value.
884 Args:
885 schema: The core schema.
887 Returns:
888 The generated JSON schema.
889 """
890 enum_type = schema['cls']
891 description = None if not enum_type.__doc__ else inspect.cleandoc(enum_type.__doc__)
892 if (
893 description == 'An enumeration.'
894 ): # This is the default value provided by enum.EnumMeta.__new__; don't use it
895 description = None
896 result: dict[str, Any] = {'title': enum_type.__name__, 'description': description}
897 result = {k: v for k, v in result.items() if v is not None}
899 expected = [to_jsonable_python(v.value) for v in schema['members']]
901 result['enum'] = expected
903 types = {type(e) for e in expected}
904 if isinstance(enum_type, str) or types == {str}:
905 result['type'] = 'string'
906 elif isinstance(enum_type, int) or types == {int}:
907 result['type'] = 'integer'
908 elif isinstance(enum_type, float) or types == {float}:
909 result['type'] = 'number'
910 elif types == {bool}:
911 result['type'] = 'boolean'
912 elif types == {list}:
913 result['type'] = 'array'
915 return result
917 def is_instance_schema(self, schema: core_schema.IsInstanceSchema) -> JsonSchemaValue:
918 """Handles JSON schema generation for a core schema that checks if a value is an instance of a class.
920 Unless overridden in a subclass, this raises an error.
922 Args:
923 schema: The core schema.
925 Returns:
926 The generated JSON schema.
927 """
928 return self.handle_invalid_for_json_schema(schema, f'core_schema.IsInstanceSchema ({schema["cls"]})')
930 def is_subclass_schema(self, schema: core_schema.IsSubclassSchema) -> JsonSchemaValue:
931 """Handles JSON schema generation for a core schema that checks if a value is a subclass of a class.
933 For backwards compatibility with v1, this does not raise an error, but can be overridden to change this.
935 Args:
936 schema: The core schema.
938 Returns:
939 The generated JSON schema.
940 """
941 # Note: This is for compatibility with V1; you can override if you want different behavior.
942 return {}
944 def callable_schema(self, schema: core_schema.CallableSchema) -> JsonSchemaValue:
945 """Generates a JSON schema that matches a callable value.
947 Unless overridden in a subclass, this raises an error.
949 Args:
950 schema: The core schema.
952 Returns:
953 The generated JSON schema.
954 """
955 return self.handle_invalid_for_json_schema(schema, 'core_schema.CallableSchema')
957 def list_schema(self, schema: core_schema.ListSchema) -> JsonSchemaValue:
958 """Returns a schema that matches a list schema.
960 Args:
961 schema: The core schema.
963 Returns:
964 The generated JSON schema.
965 """
966 items_schema = {} if 'items_schema' not in schema else self.generate_inner(schema['items_schema'])
967 json_schema = {'type': 'array', 'items': items_schema}
968 self.update_with_validations(json_schema, schema, self.ValidationsMapping.array)
969 return json_schema
971 @deprecated('`tuple_positional_schema` is deprecated. Use `tuple_schema` instead.', category=None)
972 @final
973 def tuple_positional_schema(self, schema: core_schema.TupleSchema) -> JsonSchemaValue:
974 """Replaced by `tuple_schema`."""
975 warnings.warn(
976 '`tuple_positional_schema` is deprecated. Use `tuple_schema` instead.',
977 PydanticDeprecatedSince26,
978 stacklevel=2,
979 )
980 return self.tuple_schema(schema)
982 @deprecated('`tuple_variable_schema` is deprecated. Use `tuple_schema` instead.', category=None)
983 @final
984 def tuple_variable_schema(self, schema: core_schema.TupleSchema) -> JsonSchemaValue:
985 """Replaced by `tuple_schema`."""
986 warnings.warn(
987 '`tuple_variable_schema` is deprecated. Use `tuple_schema` instead.',
988 PydanticDeprecatedSince26,
989 stacklevel=2,
990 )
991 return self.tuple_schema(schema)
993 def tuple_schema(self, schema: core_schema.TupleSchema) -> JsonSchemaValue:
994 """Generates a JSON schema that matches a tuple schema e.g. `tuple[int,
995 str, bool]` or `tuple[int, ...]`.
997 Args:
998 schema: The core schema.
1000 Returns:
1001 The generated JSON schema.
1002 """
1003 json_schema: JsonSchemaValue = {'type': 'array'}
1004 if 'variadic_item_index' in schema:
1005 variadic_item_index = schema['variadic_item_index']
1006 if variadic_item_index > 0:
1007 json_schema['minItems'] = variadic_item_index
1008 json_schema['prefixItems'] = [
1009 self.generate_inner(item) for item in schema['items_schema'][:variadic_item_index]
1010 ]
1011 if variadic_item_index + 1 == len(schema['items_schema']):
1012 # if the variadic item is the last item, then represent it faithfully
1013 json_schema['items'] = self.generate_inner(schema['items_schema'][variadic_item_index])
1014 else:
1015 # otherwise, 'items' represents the schema for the variadic
1016 # item plus the suffix, so just allow anything for simplicity
1017 # for now
1018 json_schema['items'] = True
1019 else:
1020 prefixItems = [self.generate_inner(item) for item in schema['items_schema']]
1021 if prefixItems:
1022 json_schema['prefixItems'] = prefixItems
1023 json_schema['minItems'] = len(prefixItems)
1024 json_schema['maxItems'] = len(prefixItems)
1025 self.update_with_validations(json_schema, schema, self.ValidationsMapping.array)
1026 return json_schema
1028 def set_schema(self, schema: core_schema.SetSchema) -> JsonSchemaValue:
1029 """Generates a JSON schema that matches a set schema.
1031 Args:
1032 schema: The core schema.
1034 Returns:
1035 The generated JSON schema.
1036 """
1037 return self._common_set_schema(schema)
1039 def frozenset_schema(self, schema: core_schema.FrozenSetSchema) -> JsonSchemaValue:
1040 """Generates a JSON schema that matches a frozenset schema.
1042 Args:
1043 schema: The core schema.
1045 Returns:
1046 The generated JSON schema.
1047 """
1048 return self._common_set_schema(schema)
1050 def _common_set_schema(self, schema: core_schema.SetSchema | core_schema.FrozenSetSchema) -> JsonSchemaValue:
1051 items_schema = {} if 'items_schema' not in schema else self.generate_inner(schema['items_schema'])
1052 json_schema = {'type': 'array', 'uniqueItems': True, 'items': items_schema}
1053 self.update_with_validations(json_schema, schema, self.ValidationsMapping.array)
1054 return json_schema
1056 def generator_schema(self, schema: core_schema.GeneratorSchema) -> JsonSchemaValue:
1057 """Returns a JSON schema that represents the provided GeneratorSchema.
1059 Args:
1060 schema: The 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 def dict_schema(self, schema: core_schema.DictSchema) -> JsonSchemaValue:
1071 """Generates a JSON schema that matches a dict schema.
1073 Args:
1074 schema: The core schema.
1076 Returns:
1077 The generated JSON schema.
1078 """
1079 json_schema: JsonSchemaValue = {'type': 'object'}
1081 keys_schema = self.generate_inner(schema['keys_schema']).copy() if 'keys_schema' in schema else {}
1082 if '$ref' not in keys_schema:
1083 keys_pattern = keys_schema.pop('pattern', None)
1084 # Don't give a title to patternProperties/propertyNames:
1085 keys_schema.pop('title', None)
1086 else:
1087 # Here, we assume that if the keys schema is a definition reference,
1088 # it can't be a simple string core schema (and thus no pattern can exist).
1089 # However, this is only in practice (in theory, a definition reference core
1090 # schema could be generated for a simple string schema).
1091 # Note that we avoid calling `self.resolve_ref_schema`, as it might not exist yet.
1092 keys_pattern = None
1094 values_schema = self.generate_inner(schema['values_schema']).copy() if 'values_schema' in schema else {}
1095 # don't give a title to additionalProperties:
1096 values_schema.pop('title', None)
1098 if values_schema or keys_pattern is not None:
1099 if keys_pattern is None:
1100 json_schema['additionalProperties'] = values_schema
1101 else:
1102 json_schema['patternProperties'] = {keys_pattern: values_schema}
1103 else: # for `dict[str, Any]`, we allow any key and any value, since `str` is the default key type
1104 json_schema['additionalProperties'] = True
1106 if (
1107 # The len check indicates that constraints are probably present:
1108 (keys_schema.get('type') == 'string' and len(keys_schema) > 1)
1109 # If this is a definition reference schema, it most likely has constraints:
1110 or '$ref' in keys_schema
1111 ):
1112 keys_schema.pop('type', None)
1113 json_schema['propertyNames'] = keys_schema
1115 self.update_with_validations(json_schema, schema, self.ValidationsMapping.object)
1116 return json_schema
1118 def function_before_schema(self, schema: core_schema.BeforeValidatorFunctionSchema) -> JsonSchemaValue:
1119 """Generates a JSON schema that matches a function-before schema.
1121 Args:
1122 schema: The core schema.
1124 Returns:
1125 The generated JSON schema.
1126 """
1127 if self.mode == 'validation' and (input_schema := schema.get('json_schema_input_schema')):
1128 return self.generate_inner(input_schema)
1130 return self.generate_inner(schema['schema'])
1132 def function_after_schema(self, schema: core_schema.AfterValidatorFunctionSchema) -> JsonSchemaValue:
1133 """Generates a JSON schema that matches a function-after schema.
1135 Args:
1136 schema: The core schema.
1138 Returns:
1139 The generated JSON schema.
1140 """
1141 return self.generate_inner(schema['schema'])
1143 def function_plain_schema(self, schema: core_schema.PlainValidatorFunctionSchema) -> JsonSchemaValue:
1144 """Generates a JSON schema that matches a function-plain schema.
1146 Args:
1147 schema: The core schema.
1149 Returns:
1150 The generated JSON schema.
1151 """
1152 if self.mode == 'validation' and (input_schema := schema.get('json_schema_input_schema')):
1153 return self.generate_inner(input_schema)
1155 return self.handle_invalid_for_json_schema(
1156 schema, f'core_schema.PlainValidatorFunctionSchema ({schema["function"]})'
1157 )
1159 def function_wrap_schema(self, schema: core_schema.WrapValidatorFunctionSchema) -> JsonSchemaValue:
1160 """Generates a JSON schema that matches a function-wrap schema.
1162 Args:
1163 schema: The core schema.
1165 Returns:
1166 The generated JSON schema.
1167 """
1168 if self.mode == 'validation' and (input_schema := schema.get('json_schema_input_schema')):
1169 return self.generate_inner(input_schema)
1171 return self.generate_inner(schema['schema'])
1173 def default_schema(self, schema: core_schema.WithDefaultSchema) -> JsonSchemaValue:
1174 """Generates a JSON schema that matches a schema with a default value.
1176 Args:
1177 schema: The core schema.
1179 Returns:
1180 The generated JSON schema.
1181 """
1182 json_schema = self.generate_inner(schema['schema'])
1184 default = self.get_default_value(schema)
1185 if default is NoDefault or default is MISSING:
1186 return json_schema
1188 # we reflect the application of custom plain, no-info serializers to defaults for
1189 # JSON Schemas viewed in serialization mode:
1190 # TODO: improvements along with https://github.com/pydantic/pydantic/issues/8208
1191 if self.mode == 'serialization':
1192 # `_get_ser_schema_for_default_value()` is used to unpack potentially nested validator schemas:
1193 ser_schema = _get_ser_schema_for_default_value(schema['schema'])
1194 if (
1195 ser_schema is not None
1196 and (ser_func := ser_schema.get('function'))
1197 and not (default is None and ser_schema.get('when_used') in ('unless-none', 'json-unless-none'))
1198 ):
1199 try:
1200 default = ser_func(default) # type: ignore
1201 except Exception:
1202 # It might be that the provided default needs to be validated (read: parsed) first
1203 # (assuming `validate_default` is enabled). However, we can't perform
1204 # such validation during JSON Schema generation so we don't support
1205 # this pattern for now.
1206 # (One example is when using `foo: ByteSize = '1MB'`, which validates and
1207 # serializes as an int. In this case, `ser_func` is `int` and `int('1MB')` fails).
1208 self.emit_warning(
1209 'non-serializable-default',
1210 f'Unable to serialize value {default!r} with the plain serializer; excluding default from JSON schema',
1211 )
1212 return json_schema
1214 # Sort set/frozenset defaults to ensure deterministic JSON schema generation
1215 # We only sort if len > 1 because sets of size 0 or 1 are already deterministic
1216 if isinstance(default, collections.abc.Set) and len(default) > 1:
1217 try:
1218 default = sorted(default)
1219 except TypeError: # pragma: no cover
1220 # If items aren't comparable (e.g. mixed types), we can't sort them.
1221 pass
1223 try:
1224 encoded_default = self.encode_default(default)
1225 except pydantic_core.PydanticSerializationError:
1226 self.emit_warning(
1227 'non-serializable-default',
1228 f'Default value {default} is not JSON serializable; excluding default from JSON schema',
1229 )
1230 # Return the inner schema, as though there was no default
1231 return json_schema
1233 json_schema['default'] = encoded_default
1234 return json_schema
1236 def get_default_value(self, schema: core_schema.WithDefaultSchema) -> Any:
1237 """Get the default value to be used when generating a JSON Schema for a core schema with a default.
1239 The default implementation is to use the statically defined default value. This method can be overridden
1240 if you want to make use of the default factory.
1242 Args:
1243 schema: The `'with-default'` core schema.
1245 Returns:
1246 The default value to use, or [`NoDefault`][pydantic.json_schema.NoDefault] if no default
1247 value is available.
1248 """
1249 return schema.get('default', NoDefault)
1251 def nullable_schema(self, schema: core_schema.NullableSchema) -> JsonSchemaValue:
1252 """Generates a JSON schema that matches a schema that allows null values.
1254 Args:
1255 schema: The core schema.
1257 Returns:
1258 The generated JSON schema.
1259 """
1260 null_schema = {'type': 'null'}
1261 inner_json_schema = self.generate_inner(schema['schema'])
1263 if inner_json_schema == null_schema:
1264 return null_schema
1265 else:
1266 return self.get_union_of_schemas([inner_json_schema, null_schema])
1268 def union_schema(self, schema: core_schema.UnionSchema) -> JsonSchemaValue:
1269 """Generates a JSON schema that matches a schema that allows values matching any of the given schemas.
1271 Args:
1272 schema: The core schema.
1274 Returns:
1275 The generated JSON schema.
1276 """
1277 generated: list[JsonSchemaValue] = []
1279 for choice in core_schema.iter_union_choices(schema):
1280 try:
1281 generated.append(self.generate_inner(choice))
1282 except PydanticOmit: # noqa: PERF203
1283 continue
1284 except PydanticInvalidForJsonSchema as exc:
1285 self.emit_warning('skipped-choice', exc.message)
1286 if len(generated) == 1:
1287 return generated[0]
1288 return self.get_union_of_schemas(generated)
1290 def get_union_of_schemas(self, schemas: list[JsonSchemaValue]) -> JsonSchemaValue:
1291 """Returns the JSON Schema representation for the union of the provided JSON Schemas.
1293 The result depends on the configured `'union_format'`.
1295 Args:
1296 schemas: The list of JSON Schemas to be included in the union.
1298 Returns:
1299 The JSON Schema representing the union of schemas.
1300 """
1301 if self.union_format == 'primitive_type_array':
1302 types: list[str] = []
1303 for schema in schemas:
1304 schema_types: list[str] | str | None = schema.get('type')
1305 if schema_types is None:
1306 # No type, meaning it can be a ref or an empty schema.
1307 break
1308 if not isinstance(schema_types, list):
1309 schema_types = [schema_types]
1310 if not all(t in _PRIMITIVE_JSON_SCHEMA_TYPES for t in schema_types):
1311 break
1312 if len(schema) != 1:
1313 # We only want to include types that don't have any constraints. For instance,
1314 # if `schemas = [{'type': 'string', 'maxLength': 3}, {'type': 'string', 'minLength': 5}]`,
1315 # we don't want to produce `{'type': 'string', 'maxLength': 3, 'minLength': 5}`.
1316 # Same if we have some metadata (e.g. `title`) on a specific union member, we want to preserve it.
1317 break
1319 types.extend(schema_types)
1320 else:
1321 # If we got there, all the schemas where valid to be used with the `'primitive_type_array` format
1322 return {'type': list(dict.fromkeys(types))}
1324 return self.get_flattened_anyof(schemas)
1326 def tagged_union_schema(self, schema: core_schema.TaggedUnionSchema) -> JsonSchemaValue:
1327 """Generates a JSON schema that matches a schema that allows values matching any of the given schemas, where
1328 the schemas are tagged with a discriminator field that indicates which schema should be used to validate
1329 the value.
1331 Args:
1332 schema: The core schema.
1334 Returns:
1335 The generated JSON schema.
1336 """
1337 generated: dict[str, JsonSchemaValue] = {}
1338 for k, v in schema['choices'].items():
1339 if isinstance(k, Enum):
1340 k = k.value
1341 try:
1342 # Use str(k) since keys must be strings for json; while not technically correct,
1343 # it's the closest that can be represented in valid JSON
1344 generated[str(k)] = self.generate_inner(v).copy()
1345 except PydanticOmit:
1346 continue
1347 except PydanticInvalidForJsonSchema as exc:
1348 self.emit_warning('skipped-choice', exc.message)
1350 one_of_choices = _deduplicate_schemas(generated.values())
1351 json_schema: JsonSchemaValue = {'oneOf': one_of_choices}
1353 # This reflects the v1 behavior; TODO: we should make it possible to exclude OpenAPI stuff from the JSON schema
1354 openapi_discriminator = self._extract_discriminator(schema, one_of_choices)
1355 if openapi_discriminator is not None:
1356 json_schema['discriminator'] = {
1357 'propertyName': openapi_discriminator,
1358 'mapping': {k: v.get('$ref', v) for k, v in generated.items()},
1359 }
1361 return json_schema
1363 def _extract_discriminator(
1364 self, schema: core_schema.TaggedUnionSchema, one_of_choices: list[JsonDict]
1365 ) -> str | None:
1366 """Extract a compatible OpenAPI discriminator from the schema and one_of choices that end up in the final
1367 schema."""
1368 openapi_discriminator: str | None = None
1370 if isinstance(schema['discriminator'], str):
1371 return schema['discriminator']
1373 if isinstance(schema['discriminator'], list):
1374 # If the discriminator is a single item list containing a string, that is equivalent to the string case
1375 if len(schema['discriminator']) == 1 and isinstance(schema['discriminator'][0], str):
1376 return schema['discriminator'][0]
1377 # When an alias is used that is different from the field name, the discriminator will be a list of single
1378 # str lists, one for the attribute and one for the actual alias. The logic here will work even if there is
1379 # more than one possible attribute, and looks for whether a single alias choice is present as a documented
1380 # property on all choices. If so, that property will be used as the OpenAPI discriminator.
1381 for alias_path in schema['discriminator']:
1382 if not isinstance(alias_path, list):
1383 break # this means that the discriminator is not a list of alias paths
1384 if len(alias_path) != 1:
1385 continue # this means that the "alias" does not represent a single field
1386 alias = alias_path[0]
1387 if not isinstance(alias, str):
1388 continue # this means that the "alias" does not represent a field
1389 alias_is_present_on_all_choices = True
1390 for choice in one_of_choices:
1391 try:
1392 choice = self.resolve_ref_schema(choice)
1393 except RuntimeError as exc:
1394 # TODO: fixme - this is a workaround for the fact that we can't always resolve refs
1395 # for tagged union choices at this point in the schema gen process, we might need to do
1396 # another pass at the end like we do for core schemas
1397 self.emit_warning('skipped-discriminator', str(exc))
1398 choice = {}
1399 properties = choice.get('properties', {})
1400 if not isinstance(properties, dict) or alias not in properties:
1401 alias_is_present_on_all_choices = False
1402 break
1403 if alias_is_present_on_all_choices:
1404 openapi_discriminator = alias
1405 break
1406 return openapi_discriminator
1408 def chain_schema(self, schema: core_schema.ChainSchema) -> JsonSchemaValue:
1409 """Generates a JSON schema that matches a core_schema.ChainSchema.
1411 When generating a schema for validation, we return the validation JSON schema for the first step in the chain.
1412 For serialization, we return the serialization JSON schema for the last step in the chain.
1414 Args:
1415 schema: The core schema.
1417 Returns:
1418 The generated JSON schema.
1419 """
1420 step_index = 0 if self.mode == 'validation' else -1 # use first step for validation, last for serialization
1421 return self.generate_inner(schema['steps'][step_index])
1423 def lax_or_strict_schema(self, schema: core_schema.LaxOrStrictSchema) -> JsonSchemaValue:
1424 """Generates a JSON schema that matches a schema that allows values matching either the lax schema or the
1425 strict schema.
1427 Args:
1428 schema: The core schema.
1430 Returns:
1431 The generated JSON schema.
1432 """
1433 # TODO: Need to read the default value off of model config or whatever
1434 use_strict = schema.get('strict', False) # TODO: replace this default False
1435 # If your JSON schema fails to generate it is probably
1436 # because one of the following two branches failed.
1437 if use_strict:
1438 return self.generate_inner(schema['strict_schema'])
1439 else:
1440 return self.generate_inner(schema['lax_schema'])
1442 def json_or_python_schema(self, schema: core_schema.JsonOrPythonSchema) -> JsonSchemaValue:
1443 """Generates a JSON schema that matches a schema that allows values matching either the JSON schema or the
1444 Python schema.
1446 The JSON schema is used instead of the Python schema. If you want to use the Python schema, you should override
1447 this method.
1449 Args:
1450 schema: The core schema.
1452 Returns:
1453 The generated JSON schema.
1454 """
1455 return self.generate_inner(schema['json_schema'])
1457 def typed_dict_schema(self, schema: core_schema.TypedDictSchema) -> JsonSchemaValue:
1458 """Generates a JSON schema that matches a schema that defines a typed dict.
1460 Args:
1461 schema: The core schema.
1463 Returns:
1464 The generated JSON schema.
1465 """
1466 total = schema.get('total', True)
1467 named_required_fields: list[tuple[str, bool, CoreSchemaField]] = [
1468 (name, self.field_is_required(field, total), field)
1469 for name, field in schema['fields'].items()
1470 if self.field_is_present(field)
1471 ]
1472 if self.mode == 'serialization':
1473 named_required_fields.extend(self._name_required_computed_fields(schema.get('computed_fields', [])))
1474 cls = schema.get('cls')
1475 config = _get_typed_dict_config(cls)
1476 with self._config_wrapper_stack.push(config):
1477 json_schema = self._named_required_fields_schema(named_required_fields)
1479 # There's some duplication between `extra_behavior` and
1480 # the config's `extra`/core config's `extra_fields_behavior`.
1481 # However, it is common to manually create TypedDictSchemas,
1482 # where you don't necessarily have a class.
1483 # At runtime, `extra_behavior` takes priority over the config
1484 # for validation, so follow the same for the JSON Schema:
1485 if 'extras_schema' in schema and schema['extras_schema'] != core_schema.any_schema():
1486 allow_additional_props = self.generate_inner(schema['extras_schema'])
1487 else:
1488 allow_additional_props = True
1490 if schema.get('extra_behavior') == 'forbid':
1491 json_schema['additionalProperties'] = False
1492 elif schema.get('extra_behavior') == 'allow':
1493 json_schema['additionalProperties'] = allow_additional_props
1495 if cls is not None:
1496 # `_update_class_schema()` will not override
1497 # `additionalProperties` if already present:
1498 self._update_class_schema(json_schema, cls, config)
1499 elif 'additionalProperties' not in json_schema:
1500 extra = schema.get('config', {}).get('extra_fields_behavior')
1501 if extra == 'forbid':
1502 json_schema['additionalProperties'] = False
1503 elif extra == 'allow':
1504 json_schema['additionalProperties'] = allow_additional_props
1506 return json_schema
1508 @staticmethod
1509 def _name_required_computed_fields(
1510 computed_fields: list[ComputedField],
1511 ) -> list[tuple[str, bool, core_schema.ComputedField]]:
1512 return [(field['property_name'], True, field) for field in computed_fields]
1514 def _named_required_fields_schema(
1515 self, named_required_fields: Sequence[tuple[str, bool, CoreSchemaField]]
1516 ) -> JsonSchemaValue:
1517 properties: dict[str, JsonSchemaValue] = {}
1518 required_fields: list[str] = []
1519 for name, required, field in named_required_fields:
1520 if self.by_alias:
1521 name = self._get_alias_name(field, name)
1522 try:
1523 field_json_schema = self.generate_inner(field).copy()
1524 except PydanticOmit:
1525 continue
1526 if 'title' not in field_json_schema and self.field_title_should_be_set(field):
1527 title = self.get_title_from_name(name)
1528 field_json_schema['title'] = title
1529 field_json_schema = self.handle_ref_overrides(field_json_schema)
1530 properties[name] = field_json_schema
1531 if required:
1532 required_fields.append(name)
1534 json_schema = {'type': 'object', 'properties': properties}
1535 if required_fields:
1536 json_schema['required'] = required_fields
1537 return json_schema
1539 def _get_alias_name(self, field: CoreSchemaField, name: str) -> str:
1540 if field['type'] == 'computed-field':
1541 alias: Any = field.get('alias', name)
1542 elif self.mode == 'validation':
1543 alias = field.get('validation_alias', name)
1544 else:
1545 alias = field.get('serialization_alias', name)
1546 if isinstance(alias, str):
1547 name = alias
1548 elif isinstance(alias, list):
1549 alias = cast('list[str] | str', alias)
1550 for path in alias:
1551 if isinstance(path, list) and len(path) == 1 and isinstance(path[0], str):
1552 # Use the first valid single-item string path; the code that constructs the alias array
1553 # should ensure the first such item is what belongs in the JSON schema
1554 name = path[0]
1555 break
1556 else:
1557 assert_never(alias)
1558 return name
1560 def typed_dict_field_schema(self, schema: core_schema.TypedDictField) -> JsonSchemaValue:
1561 """Generates a JSON schema that matches a schema that defines a typed dict field.
1563 Args:
1564 schema: The core schema.
1566 Returns:
1567 The generated JSON schema.
1568 """
1569 return self.generate_inner(schema['schema'])
1571 def dataclass_field_schema(self, schema: core_schema.DataclassField) -> JsonSchemaValue:
1572 """Generates a JSON schema that matches a schema that defines a dataclass field.
1574 Args:
1575 schema: The core schema.
1577 Returns:
1578 The generated JSON schema.
1579 """
1580 return self.generate_inner(schema['schema'])
1582 def model_field_schema(self, schema: core_schema.ModelField) -> JsonSchemaValue:
1583 """Generates a JSON schema that matches a schema that defines a model field.
1585 Args:
1586 schema: The core schema.
1588 Returns:
1589 The generated JSON schema.
1590 """
1591 return self.generate_inner(schema['schema'])
1593 def computed_field_schema(self, schema: core_schema.ComputedField) -> JsonSchemaValue:
1594 """Generates a JSON schema that matches a schema that defines a computed field.
1596 Args:
1597 schema: The core schema.
1599 Returns:
1600 The generated JSON schema.
1601 """
1602 return self.generate_inner(schema['return_schema'])
1604 def model_schema(self, schema: core_schema.ModelSchema) -> JsonSchemaValue:
1605 """Generates a JSON schema that matches a schema that defines a model.
1607 Args:
1608 schema: The core schema.
1610 Returns:
1611 The generated JSON schema.
1612 """
1613 # We do not use schema['model'].model_json_schema() here
1614 # because it could lead to inconsistent refs handling, etc.
1615 cls = cast('type[BaseModel]', schema['cls'])
1616 config = cls.model_config
1618 with self._config_wrapper_stack.push(config):
1619 json_schema = self.generate_inner(schema['schema'])
1621 self._update_class_schema(json_schema, cls, config)
1623 return json_schema
1625 def _update_class_schema(self, json_schema: JsonSchemaValue, cls: type[Any], config: ConfigDict) -> None:
1626 """Update json_schema with the following, extracted from `config` and `cls`:
1628 * title
1629 * description
1630 * additional properties
1631 * json_schema_extra
1632 * deprecated
1634 Done in place, hence there's no return value as the original json_schema is mutated.
1635 No ref resolving is involved here, as that's not appropriate for simple updates.
1636 """
1637 from ._internal._dataclasses import is_stdlib_dataclass
1638 from .main import BaseModel
1639 from .root_model import RootModel
1641 if (config_title := config.get('title')) is not None:
1642 json_schema.setdefault('title', config_title)
1643 elif model_title_generator := config.get('model_title_generator'):
1644 title = model_title_generator(cls)
1645 if not isinstance(title, str):
1646 raise TypeError(f'model_title_generator {model_title_generator} must return str, not {title.__class__}')
1647 json_schema.setdefault('title', title)
1648 if 'title' not in json_schema:
1649 json_schema['title'] = cls.__name__
1651 # BaseModel and dataclasses; don't use cls.__doc__ as it will contain the verbose class signature by default
1652 if cls is BaseModel:
1653 docstring = None
1654 elif is_stdlib_dataclass(cls): # For Pydantic dataclasses, we already handle this at class creation
1655 # The `dataclass` module generates a `__doc__` based on the `inspect.signature()`
1656 # result, which we don't want to use as a description. Such `__doc__` startswith
1657 # `cls.__name__(`, which could lead to mistakenly discarding it if for some reason
1658 # an explicitly set class docstring follows the same pattern, but this is unlikely
1659 # to happen.
1660 doc = cls.__doc__
1661 docstring = None if doc is None or doc.startswith(f'{cls.__name__}(') else doc
1662 else:
1663 docstring = cls.__doc__
1665 if docstring:
1666 json_schema.setdefault('description', inspect.cleandoc(docstring))
1667 elif issubclass(cls, RootModel) and (root_description := cls.__pydantic_fields__['root'].description):
1668 json_schema.setdefault('description', root_description)
1670 extra = config.get('extra')
1671 if 'additionalProperties' not in json_schema: # This check is particularly important for `typed_dict_schema()`
1672 if extra == 'allow':
1673 json_schema['additionalProperties'] = True
1674 elif extra == 'forbid':
1675 json_schema['additionalProperties'] = False
1677 json_schema_extra = config.get('json_schema_extra')
1678 if issubclass(cls, BaseModel) and cls.__pydantic_root_model__:
1679 root_json_schema_extra = cls.model_fields['root'].json_schema_extra
1680 if json_schema_extra and root_json_schema_extra:
1681 raise ValueError(
1682 '"model_config[\'json_schema_extra\']" and "Field.json_schema_extra" on "RootModel.root"'
1683 ' field must not be set simultaneously'
1684 )
1685 if root_json_schema_extra:
1686 json_schema_extra = root_json_schema_extra
1688 if isinstance(json_schema_extra, (staticmethod, classmethod)):
1689 # In older versions of python, this is necessary to ensure staticmethod/classmethods are callable
1690 json_schema_extra = json_schema_extra.__get__(cls)
1692 if isinstance(json_schema_extra, dict):
1693 json_schema.update(json_schema_extra)
1694 elif callable(json_schema_extra):
1695 if len(_typing_extra.signature_no_eval(json_schema_extra).parameters) > 1:
1696 json_schema_extra = cast(Callable[[JsonDict, type[Any]], None], json_schema_extra)
1697 json_schema_extra(json_schema, cls)
1698 else:
1699 json_schema_extra = cast(Callable[[JsonDict], None], json_schema_extra)
1700 json_schema_extra(json_schema)
1701 elif json_schema_extra is not None:
1702 raise ValueError(
1703 f"model_config['json_schema_extra']={json_schema_extra} should be a dict, callable, or None"
1704 )
1706 if hasattr(cls, '__deprecated__'):
1707 json_schema['deprecated'] = True
1709 def resolve_ref_schema(self, json_schema: JsonSchemaValue) -> JsonSchemaValue:
1710 """Resolve a JsonSchemaValue to the non-ref schema if it is a $ref schema.
1712 Args:
1713 json_schema: The schema to resolve.
1715 Returns:
1716 The resolved schema.
1718 Raises:
1719 RuntimeError: If the schema reference can't be found in definitions.
1720 """
1721 while '$ref' in json_schema:
1722 ref = json_schema['$ref']
1723 schema_to_update = self.get_schema_from_definitions(JsonRef(ref))
1724 if schema_to_update is None:
1725 raise RuntimeError(f'Cannot update undefined schema for $ref={ref}')
1726 json_schema = schema_to_update
1727 return json_schema
1729 def model_fields_schema(self, schema: core_schema.ModelFieldsSchema) -> JsonSchemaValue:
1730 """Generates a JSON schema that matches a schema that defines a model's fields.
1732 Args:
1733 schema: The core schema.
1735 Returns:
1736 The generated JSON schema.
1737 """
1738 named_required_fields: list[tuple[str, bool, CoreSchemaField]] = [
1739 (name, self.field_is_required(field, total=True), field)
1740 for name, field in schema['fields'].items()
1741 if self.field_is_present(field)
1742 ]
1743 if self.mode == 'serialization':
1744 named_required_fields.extend(self._name_required_computed_fields(schema.get('computed_fields', [])))
1745 json_schema = self._named_required_fields_schema(named_required_fields)
1746 extras_schema = schema.get('extras_schema', None)
1747 if extras_schema is not None:
1748 schema_to_update = self.resolve_ref_schema(json_schema)
1749 schema_to_update['additionalProperties'] = self.generate_inner(extras_schema)
1750 return json_schema
1752 def field_is_present(self, field: CoreSchemaField) -> bool:
1753 """Whether the field should be included in the generated JSON schema.
1755 Args:
1756 field: The schema for the field itself.
1758 Returns:
1759 `True` if the field should be included in the generated JSON schema, `False` otherwise.
1760 """
1761 if self.mode == 'serialization':
1762 # If you still want to include the field in the generated JSON schema,
1763 # override this method and return True
1764 return not field.get('serialization_exclude')
1765 elif self.mode == 'validation':
1766 return True
1767 else:
1768 assert_never(self.mode)
1770 def field_is_required(
1771 self,
1772 field: core_schema.ModelField | core_schema.DataclassField | core_schema.TypedDictField,
1773 total: bool,
1774 ) -> bool:
1775 """Whether the field should be marked as required in the generated JSON schema.
1776 (Note that this is irrelevant if the field is not present in the JSON schema.).
1778 Args:
1779 field: The schema for the field itself.
1780 total: Only applies to `TypedDictField`s.
1781 Indicates if the `TypedDict` this field belongs to is total, in which case any fields that don't
1782 explicitly specify `required=False` are required.
1784 Returns:
1785 `True` if the field should be marked as required in the generated JSON schema, `False` otherwise.
1786 """
1787 if field['type'] == 'typed-dict-field':
1788 required = field.get('required', total)
1789 else:
1790 required = field['schema']['type'] != 'default'
1792 if self.mode == 'serialization':
1793 has_exclude_if = field.get('serialization_exclude_if') is not None
1794 if self._config.json_schema_serialization_defaults_required:
1795 return not has_exclude_if
1796 else:
1797 return required and not has_exclude_if
1798 else:
1799 return required
1801 def dataclass_args_schema(self, schema: core_schema.DataclassArgsSchema) -> JsonSchemaValue:
1802 """Generates a JSON schema that matches a schema that defines a dataclass's constructor arguments.
1804 Args:
1805 schema: The core schema.
1807 Returns:
1808 The generated JSON schema.
1809 """
1810 named_required_fields: list[tuple[str, bool, CoreSchemaField]] = [
1811 (field['name'], self.field_is_required(field, total=True), field)
1812 for field in schema['fields']
1813 if self.field_is_present(field)
1814 ]
1815 if self.mode == 'serialization':
1816 named_required_fields.extend(self._name_required_computed_fields(schema.get('computed_fields', [])))
1817 return self._named_required_fields_schema(named_required_fields)
1819 def dataclass_schema(self, schema: core_schema.DataclassSchema) -> JsonSchemaValue:
1820 """Generates a JSON schema that matches a schema that defines a dataclass.
1822 Args:
1823 schema: The core schema.
1825 Returns:
1826 The generated JSON schema.
1827 """
1829 cls = schema['cls']
1830 config = cast('ConfigDict', getattr(cls, '__pydantic_config__', {}))
1832 with self._config_wrapper_stack.push(config):
1833 json_schema = self.generate_inner(schema['schema']).copy()
1835 self._update_class_schema(json_schema, cls, config)
1837 return json_schema
1839 def arguments_schema(self, schema: core_schema.ArgumentsSchema) -> JsonSchemaValue:
1840 """Generates a JSON schema that matches a schema that defines a function's arguments.
1842 Args:
1843 schema: The core schema.
1845 Returns:
1846 The generated JSON schema.
1847 """
1848 prefer_positional = schema.get('metadata', {}).get('pydantic_js_prefer_positional_arguments')
1850 arguments = schema['arguments_schema']
1851 kw_only_arguments = [a for a in arguments if a.get('mode') == 'keyword_only']
1852 kw_or_p_arguments = [a for a in arguments if a.get('mode') in {'positional_or_keyword', None}]
1853 p_only_arguments = [a for a in arguments if a.get('mode') == 'positional_only']
1854 var_args_schema = schema.get('var_args_schema')
1855 var_kwargs_schema = schema.get('var_kwargs_schema')
1857 if prefer_positional:
1858 positional_possible = not kw_only_arguments and not var_kwargs_schema
1859 if positional_possible:
1860 return self.p_arguments_schema(p_only_arguments + kw_or_p_arguments, var_args_schema)
1862 keyword_possible = not p_only_arguments and not var_args_schema
1863 if keyword_possible:
1864 return self.kw_arguments_schema(kw_or_p_arguments + kw_only_arguments, var_kwargs_schema)
1866 if not prefer_positional:
1867 positional_possible = not kw_only_arguments and not var_kwargs_schema
1868 if positional_possible:
1869 return self.p_arguments_schema(p_only_arguments + kw_or_p_arguments, var_args_schema)
1871 raise PydanticInvalidForJsonSchema(
1872 'Unable to generate JSON schema for arguments validator with positional-only and keyword-only arguments'
1873 )
1875 def kw_arguments_schema(
1876 self, arguments: list[core_schema.ArgumentsParameter], var_kwargs_schema: CoreSchema | None
1877 ) -> JsonSchemaValue:
1878 """Generates a JSON schema that matches a schema that defines a function's keyword arguments.
1880 Args:
1881 arguments: The core schema.
1883 Returns:
1884 The generated JSON schema.
1885 """
1886 properties: dict[str, JsonSchemaValue] = {}
1887 required: list[str] = []
1888 for argument in arguments:
1889 name = self.get_argument_name(argument)
1890 argument_schema = self.generate_inner(argument['schema']).copy()
1891 if 'title' not in argument_schema and self.field_title_should_be_set(argument['schema']):
1892 argument_schema['title'] = self.get_title_from_name(name)
1893 properties[name] = argument_schema
1895 if argument['schema']['type'] != 'default':
1896 # This assumes that if the argument has a default value,
1897 # the inner schema must be of type WithDefaultSchema.
1898 # I believe this is true, but I am not 100% sure
1899 required.append(name)
1901 json_schema: JsonSchemaValue = {'type': 'object', 'properties': properties}
1902 if required:
1903 json_schema['required'] = required
1905 if var_kwargs_schema:
1906 additional_properties_schema = self.generate_inner(var_kwargs_schema)
1907 if additional_properties_schema:
1908 json_schema['additionalProperties'] = additional_properties_schema
1909 else:
1910 json_schema['additionalProperties'] = False
1911 return json_schema
1913 def p_arguments_schema(
1914 self, arguments: list[core_schema.ArgumentsParameter], var_args_schema: CoreSchema | None
1915 ) -> JsonSchemaValue:
1916 """Generates a JSON schema that matches a schema that defines a function's positional arguments.
1918 Args:
1919 arguments: The core schema.
1921 Returns:
1922 The generated JSON schema.
1923 """
1924 prefix_items: list[JsonSchemaValue] = []
1925 min_items = 0
1927 for argument in arguments:
1928 name = self.get_argument_name(argument)
1930 argument_schema = self.generate_inner(argument['schema']).copy()
1931 if 'title' not in argument_schema and self.field_title_should_be_set(argument['schema']):
1932 argument_schema['title'] = self.get_title_from_name(name)
1933 prefix_items.append(argument_schema)
1935 if argument['schema']['type'] != 'default':
1936 # This assumes that if the argument has a default value,
1937 # the inner schema must be of type WithDefaultSchema.
1938 # I believe this is true, but I am not 100% sure
1939 min_items += 1
1941 json_schema: JsonSchemaValue = {'type': 'array'}
1942 if prefix_items:
1943 json_schema['prefixItems'] = prefix_items
1944 if min_items:
1945 json_schema['minItems'] = min_items
1947 if var_args_schema:
1948 items_schema = self.generate_inner(var_args_schema)
1949 if items_schema:
1950 json_schema['items'] = items_schema
1951 else:
1952 json_schema['maxItems'] = len(prefix_items)
1954 return json_schema
1956 def get_argument_name(self, argument: core_schema.ArgumentsParameter | core_schema.ArgumentsV3Parameter) -> str:
1957 """Retrieves the name of an argument.
1959 Args:
1960 argument: The core schema.
1962 Returns:
1963 The name of the argument.
1964 """
1965 name = argument['name']
1966 if self.by_alias:
1967 alias = argument.get('alias')
1968 if isinstance(alias, str):
1969 name = alias
1970 else:
1971 pass # might want to do something else?
1972 return name
1974 def arguments_v3_schema(self, schema: core_schema.ArgumentsV3Schema) -> JsonSchemaValue:
1975 """Generates a JSON schema that matches a schema that defines a function's arguments.
1977 Args:
1978 schema: The core schema.
1980 Returns:
1981 The generated JSON schema.
1982 """
1983 arguments = schema['arguments_schema']
1984 properties: dict[str, JsonSchemaValue] = {}
1985 required: list[str] = []
1986 for argument in arguments:
1987 mode = argument.get('mode', 'positional_or_keyword')
1988 name = self.get_argument_name(argument)
1989 argument_schema = self.generate_inner(argument['schema']).copy()
1990 if mode == 'var_args':
1991 argument_schema = {'type': 'array', 'items': argument_schema}
1992 elif mode == 'var_kwargs_uniform':
1993 argument_schema = {'type': 'object', 'additionalProperties': argument_schema}
1995 argument_schema.setdefault('title', self.get_title_from_name(name))
1996 properties[name] = argument_schema
1998 if (
1999 (mode == 'var_kwargs_unpacked_typed_dict' and 'required' in argument_schema)
2000 or mode not in {'var_args', 'var_kwargs_uniform', 'var_kwargs_unpacked_typed_dict'}
2001 and argument['schema']['type'] != 'default'
2002 ):
2003 # This assumes that if the argument has a default value,
2004 # the inner schema must be of type WithDefaultSchema.
2005 # I believe this is true, but I am not 100% sure
2006 required.append(name)
2008 json_schema: JsonSchemaValue = {'type': 'object', 'properties': properties}
2009 if required:
2010 json_schema['required'] = required
2011 return json_schema
2013 def call_schema(self, schema: core_schema.CallSchema) -> JsonSchemaValue:
2014 """Generates a JSON schema that matches a schema that defines a function call.
2016 Args:
2017 schema: The core schema.
2019 Returns:
2020 The generated JSON schema.
2021 """
2022 return self.generate_inner(schema['arguments_schema'])
2024 def custom_error_schema(self, schema: core_schema.CustomErrorSchema) -> JsonSchemaValue:
2025 """Generates a JSON schema that matches a schema that defines a custom error.
2027 Args:
2028 schema: The core schema.
2030 Returns:
2031 The generated JSON schema.
2032 """
2033 return self.generate_inner(schema['schema'])
2035 def json_schema(self, schema: core_schema.JsonSchema) -> JsonSchemaValue:
2036 """Generates a JSON schema that matches a schema that defines a JSON object.
2038 Args:
2039 schema: The core schema.
2041 Returns:
2042 The generated JSON schema.
2043 """
2044 content_core_schema = schema.get('schema') or core_schema.any_schema()
2045 content_json_schema = self.generate_inner(content_core_schema)
2046 if self.mode == 'validation':
2047 return {'type': 'string', 'contentMediaType': 'application/json', 'contentSchema': content_json_schema}
2048 else:
2049 # self.mode == 'serialization'
2050 return content_json_schema
2052 def url_schema(self, schema: core_schema.UrlSchema) -> JsonSchemaValue:
2053 """Generates a JSON schema that matches a schema that defines a URL.
2055 Args:
2056 schema: The core schema.
2058 Returns:
2059 The generated JSON schema.
2060 """
2061 json_schema = {'type': 'string', 'format': 'uri', 'minLength': 1}
2062 self.update_with_validations(json_schema, schema, self.ValidationsMapping.string)
2063 return json_schema
2065 def multi_host_url_schema(self, schema: core_schema.MultiHostUrlSchema) -> JsonSchemaValue:
2066 """Generates a JSON schema that matches a schema that defines a URL that can be used with multiple hosts.
2068 Args:
2069 schema: The core schema.
2071 Returns:
2072 The generated JSON schema.
2073 """
2074 # Note: 'multi-host-uri' is a custom/pydantic-specific format, not part of the JSON Schema spec
2075 json_schema = {'type': 'string', 'format': 'multi-host-uri', 'minLength': 1}
2076 self.update_with_validations(json_schema, schema, self.ValidationsMapping.string)
2077 return json_schema
2079 def uuid_schema(self, schema: core_schema.UuidSchema) -> JsonSchemaValue:
2080 """Generates a JSON schema that matches a UUID.
2082 Args:
2083 schema: The core schema.
2085 Returns:
2086 The generated JSON schema.
2087 """
2088 return {'type': 'string', 'format': 'uuid'}
2090 def definitions_schema(self, schema: core_schema.DefinitionsSchema) -> JsonSchemaValue:
2091 """Generates a JSON schema that matches a schema that defines a JSON object with definitions.
2093 Args:
2094 schema: The core schema.
2096 Returns:
2097 The generated JSON schema.
2098 """
2099 for definition in schema['definitions']:
2100 try:
2101 self.generate_inner(definition)
2102 except PydanticInvalidForJsonSchema as e: # noqa: PERF203
2103 core_ref: CoreRef = CoreRef(definition['ref']) # type: ignore
2104 self._core_defs_invalid_for_json_schema[self.get_defs_ref((core_ref, self.mode))] = e
2105 continue
2106 return self.generate_inner(schema['schema'])
2108 def definition_ref_schema(self, schema: core_schema.DefinitionReferenceSchema) -> JsonSchemaValue:
2109 """Generates a JSON schema that matches a schema that references a definition.
2111 Args:
2112 schema: The core schema.
2114 Returns:
2115 The generated JSON schema.
2116 """
2117 core_ref = CoreRef(schema['schema_ref'])
2118 _, ref_json_schema = self.get_cache_defs_ref_schema(core_ref)
2119 return ref_json_schema
2121 def ser_schema(
2122 self, schema: core_schema.SerSchema | core_schema.IncExSeqSerSchema | core_schema.IncExDictSerSchema
2123 ) -> JsonSchemaValue | None:
2124 """Generates a JSON schema that matches a schema that defines a serialized object.
2126 Args:
2127 schema: The core schema.
2129 Returns:
2130 The generated JSON schema.
2131 """
2132 schema_type = schema['type']
2133 if schema_type == 'function-plain' or schema_type == 'function-wrap':
2134 # PlainSerializerFunctionSerSchema or WrapSerializerFunctionSerSchema
2135 return_schema = schema.get('return_schema')
2136 if return_schema is not None:
2137 return self.generate_inner(return_schema)
2138 elif schema_type == 'format' or schema_type == 'to-string':
2139 # FormatSerSchema or ToStringSerSchema
2140 return self.str_schema(core_schema.str_schema())
2141 elif schema['type'] == 'model':
2142 # ModelSerSchema
2143 return self.generate_inner(schema['schema'])
2144 return None
2146 def complex_schema(self, schema: core_schema.ComplexSchema) -> JsonSchemaValue:
2147 """Generates a JSON schema that matches a complex number.
2149 JSON has no standard way to represent complex numbers. Complex number is not a numeric
2150 type. Here we represent complex number as strings following the rule defined by Python.
2151 For instance, '1+2j' is an accepted complex string. Details can be found in
2152 [Python's `complex` documentation][complex].
2154 Args:
2155 schema: The core schema.
2157 Returns:
2158 The generated JSON schema.
2159 """
2160 return {'type': 'string'}
2162 # ### Utility methods
2164 def get_title_from_name(self, name: str) -> str:
2165 """Retrieves a title from a name.
2167 Args:
2168 name: The name to retrieve a title from.
2170 Returns:
2171 The title.
2172 """
2173 return name.title().replace('_', ' ').strip()
2175 def field_title_should_be_set(self, schema: CoreSchemaOrField) -> bool:
2176 """Returns true if a field with the given schema should have a title set based on the field name.
2178 Intuitively, we want this to return true for schemas that wouldn't otherwise provide their own title
2179 (e.g., int, float, str), and false for those that would (e.g., BaseModel subclasses).
2181 Args:
2182 schema: The schema to check.
2184 Returns:
2185 `True` if the field should have a title set, `False` otherwise.
2186 """
2187 if _core_utils.is_core_schema_field(schema):
2188 if schema['type'] == 'computed-field':
2189 field_schema = schema['return_schema']
2190 else:
2191 field_schema = schema['schema']
2192 return self.field_title_should_be_set(field_schema)
2194 elif _core_utils.is_core_schema(schema):
2195 if schema.get('ref'): # things with refs, such as models and enums, should not have titles set
2196 return False
2197 if schema['type'] in {'default', 'nullable', 'definitions'}:
2198 return self.field_title_should_be_set(schema['schema']) # type: ignore[typeddict-item]
2199 if _core_utils.is_function_with_inner_schema(schema):
2200 return self.field_title_should_be_set(schema['schema'])
2201 if schema['type'] == 'definition-ref':
2202 # Referenced schemas should not have titles set for the same reason
2203 # schemas with refs should not
2204 return False
2205 return True # anything else should have title set
2207 else:
2208 raise PydanticInvalidForJsonSchema(f'Unexpected schema type: schema={schema}') # pragma: no cover
2210 def normalize_name(self, name: str) -> str:
2211 """Normalizes a name to be used as a key in a dictionary.
2213 Args:
2214 name: The name to normalize.
2216 Returns:
2217 The normalized name.
2218 """
2219 return re.sub(r'[^a-zA-Z0-9.\-_]', '_', name).replace('.', '__')
2221 def get_defs_ref(self, core_mode_ref: CoreModeRef) -> DefsRef:
2222 """Override this method to change the way that definitions keys are generated from a core reference.
2224 Args:
2225 core_mode_ref: The core reference.
2227 Returns:
2228 The definitions key.
2229 """
2230 # Split the core ref into "components"; generic origins and arguments are each separate components
2231 core_ref, mode = core_mode_ref
2232 components = re.split(r'([\][,])', core_ref)
2233 # Remove IDs from each component
2234 components = [x.rsplit(':', 1)[0] for x in components]
2235 core_ref_no_id = ''.join(components)
2236 # Remove everything before the last period from each "component"
2237 components = [re.sub(r'(?:[^.[\]]+\.)+((?:[^.[\]]+))', r'\1', x) for x in components]
2238 short_ref = ''.join(components)
2240 mode_title = _MODE_TITLE_MAPPING[mode]
2242 # It is important that the generated defs_ref values be such that at least one choice will not
2243 # be generated for any other core_ref. Currently, this should be the case because we include
2244 # the id of the source type in the core_ref
2245 name = DefsRef(self.normalize_name(short_ref))
2246 name_mode = DefsRef(self.normalize_name(short_ref) + f'-{mode_title}')
2247 module_qualname = DefsRef(self.normalize_name(core_ref_no_id))
2248 module_qualname_mode = DefsRef(f'{module_qualname}-{mode_title}')
2249 module_qualname_id = DefsRef(self.normalize_name(core_ref))
2250 occurrence_index = self._collision_index.get(module_qualname_id)
2251 if occurrence_index is None:
2252 self._collision_counter[module_qualname] += 1
2253 occurrence_index = self._collision_index[module_qualname_id] = self._collision_counter[module_qualname]
2255 module_qualname_occurrence = DefsRef(f'{module_qualname}__{occurrence_index}')
2256 module_qualname_occurrence_mode = DefsRef(f'{module_qualname_mode}__{occurrence_index}')
2258 self._prioritized_defsref_choices[module_qualname_occurrence_mode] = [
2259 name,
2260 name_mode,
2261 module_qualname,
2262 module_qualname_mode,
2263 module_qualname_occurrence,
2264 module_qualname_occurrence_mode,
2265 ]
2267 return module_qualname_occurrence_mode
2269 def get_cache_defs_ref_schema(self, core_ref: CoreRef) -> tuple[DefsRef, JsonSchemaValue]:
2270 """This method wraps the get_defs_ref method with some cache-lookup/population logic,
2271 and returns both the produced defs_ref and the JSON schema that will refer to the right definition.
2273 Args:
2274 core_ref: The core reference to get the definitions reference for.
2276 Returns:
2277 A tuple of the definitions reference and the JSON schema that will refer to it.
2278 """
2279 core_mode_ref = (core_ref, self.mode)
2280 maybe_defs_ref = self.core_to_defs_refs.get(core_mode_ref)
2281 if maybe_defs_ref is not None:
2282 json_ref = self.core_to_json_refs[core_mode_ref]
2283 return maybe_defs_ref, {'$ref': json_ref}
2285 defs_ref = self.get_defs_ref(core_mode_ref)
2287 # populate the ref translation mappings
2288 self.core_to_defs_refs[core_mode_ref] = defs_ref
2289 self.defs_to_core_refs[defs_ref] = core_mode_ref
2291 json_ref = JsonRef(self.ref_template.format(model=defs_ref))
2292 self.core_to_json_refs[core_mode_ref] = json_ref
2293 self.json_to_defs_refs[json_ref] = defs_ref
2294 ref_json_schema = {'$ref': json_ref}
2295 return defs_ref, ref_json_schema
2297 def handle_ref_overrides(self, json_schema: JsonSchemaValue) -> JsonSchemaValue:
2298 """Remove any sibling keys that are redundant with the referenced schema.
2300 Args:
2301 json_schema: The schema to remove redundant sibling keys from.
2303 Returns:
2304 The schema with redundant sibling keys removed.
2305 """
2306 if '$ref' in json_schema:
2307 # prevent modifications to the input; this copy may be safe to drop if there is significant overhead
2308 json_schema = json_schema.copy()
2310 referenced_json_schema = self.get_schema_from_definitions(JsonRef(json_schema['$ref']))
2311 if referenced_json_schema is None:
2312 # This can happen when building schemas for models with not-yet-defined references.
2313 # It may be a good idea to do a recursive pass at the end of the generation to remove
2314 # any redundant override keys.
2315 return json_schema
2316 for k, v in list(json_schema.items()):
2317 if k == '$ref':
2318 continue
2319 if k in referenced_json_schema and referenced_json_schema[k] == v:
2320 del json_schema[k] # redundant key
2322 return json_schema
2324 def get_schema_from_definitions(self, json_ref: JsonRef) -> JsonSchemaValue | None:
2325 try:
2326 def_ref = self.json_to_defs_refs[json_ref]
2327 if def_ref in self._core_defs_invalid_for_json_schema:
2328 raise self._core_defs_invalid_for_json_schema[def_ref]
2329 return self.definitions.get(def_ref, None)
2330 except KeyError:
2331 if json_ref.startswith(('http://', 'https://')):
2332 return None
2333 raise
2335 def encode_default(self, dft: Any) -> Any:
2336 """Encode a default value to a JSON-serializable value.
2338 This is used to encode default values for fields in the generated JSON schema.
2340 Args:
2341 dft: The default value to encode.
2343 Returns:
2344 The encoded default value.
2345 """
2346 from .type_adapter import TypeAdapter, _type_has_config
2348 config = self._config
2349 try:
2350 default = (
2351 dft
2352 if _type_has_config(type(dft))
2353 else TypeAdapter(type(dft), config=config.config_dict).dump_python(
2354 dft, by_alias=self.by_alias, mode='json'
2355 )
2356 )
2357 except PydanticSchemaGenerationError:
2358 raise pydantic_core.PydanticSerializationError(f'Unable to encode default value {dft}')
2360 return pydantic_core.to_jsonable_python(
2361 default, timedelta_mode=config.ser_json_timedelta, bytes_mode=config.ser_json_bytes, by_alias=self.by_alias
2362 )
2364 def update_with_validations(
2365 self, json_schema: JsonSchemaValue, core_schema: CoreSchema, mapping: dict[str, str]
2366 ) -> None:
2367 """Update the json_schema with the corresponding validations specified in the core_schema,
2368 using the provided mapping to translate keys in core_schema to the appropriate keys for a JSON schema.
2370 Args:
2371 json_schema: The JSON schema to update.
2372 core_schema: The core schema to get the validations from.
2373 mapping: A mapping from core_schema attribute names to the corresponding JSON schema attribute names.
2374 """
2375 for core_key, json_schema_key in mapping.items():
2376 if core_key in core_schema:
2377 json_schema[json_schema_key] = core_schema[core_key]
2379 class ValidationsMapping:
2380 """This class just contains mappings from core_schema attribute names to the corresponding
2381 JSON schema attribute names. While I suspect it is unlikely to be necessary, you can in
2382 principle override this class in a subclass of GenerateJsonSchema (by inheriting from
2383 GenerateJsonSchema.ValidationsMapping) to change these mappings.
2384 """
2386 numeric = {
2387 'multiple_of': 'multipleOf',
2388 'le': 'maximum',
2389 'ge': 'minimum',
2390 'lt': 'exclusiveMaximum',
2391 'gt': 'exclusiveMinimum',
2392 }
2393 bytes = {
2394 'min_length': 'minLength',
2395 'max_length': 'maxLength',
2396 }
2397 string = {
2398 'min_length': 'minLength',
2399 'max_length': 'maxLength',
2400 'pattern': 'pattern',
2401 }
2402 array = {
2403 'min_length': 'minItems',
2404 'max_length': 'maxItems',
2405 }
2406 object = {
2407 'min_length': 'minProperties',
2408 'max_length': 'maxProperties',
2409 }
2411 def get_flattened_anyof(self, schemas: list[JsonSchemaValue]) -> JsonSchemaValue:
2412 members = []
2413 for schema in schemas:
2414 if len(schema) == 1 and 'anyOf' in schema:
2415 members.extend(schema['anyOf'])
2416 else:
2417 members.append(schema)
2418 members = _deduplicate_schemas(members)
2419 if len(members) == 1:
2420 return members[0]
2421 return {'anyOf': members}
2423 def get_json_ref_counts(self, json_schema: JsonSchemaValue) -> dict[JsonRef, int]:
2424 """Get all values corresponding to the key '$ref' anywhere in the json_schema."""
2425 json_refs: dict[JsonRef, int] = Counter()
2427 def _add_json_refs(schema: Any) -> None:
2428 if isinstance(schema, dict):
2429 if '$ref' in schema:
2430 json_ref = JsonRef(schema['$ref'])
2431 if not isinstance(json_ref, str):
2432 return # in this case, '$ref' might have been the name of a property
2433 already_visited = json_ref in json_refs
2434 json_refs[json_ref] += 1
2435 if already_visited:
2436 return # prevent recursion on a definition that was already visited
2437 try:
2438 defs_ref = self.json_to_defs_refs[json_ref]
2439 if defs_ref in self._core_defs_invalid_for_json_schema:
2440 raise self._core_defs_invalid_for_json_schema[defs_ref]
2441 _add_json_refs(self.definitions[defs_ref])
2442 except KeyError:
2443 if not json_ref.startswith(('http://', 'https://')):
2444 raise
2446 for k, v in schema.items():
2447 if k == 'examples' and isinstance(v, list):
2448 # Skip examples that may contain arbitrary values and references
2449 # (see the comment in `_get_all_json_refs` for more details).
2450 continue
2451 _add_json_refs(v)
2452 elif isinstance(schema, list):
2453 for v in schema:
2454 _add_json_refs(v)
2456 _add_json_refs(json_schema)
2457 return json_refs
2459 def handle_invalid_for_json_schema(self, schema: CoreSchemaOrField, error_info: str) -> JsonSchemaValue:
2460 raise PydanticInvalidForJsonSchema(f'Cannot generate a JsonSchema for {error_info}')
2462 def emit_warning(self, kind: JsonSchemaWarningKind, detail: str) -> None:
2463 """This method simply emits PydanticJsonSchemaWarnings based on handling in the `warning_message` method."""
2464 message = self.render_warning_message(kind, detail)
2465 if message is not None:
2466 warnings.warn(message, PydanticJsonSchemaWarning)
2468 def render_warning_message(self, kind: JsonSchemaWarningKind, detail: str) -> str | None:
2469 """This method is responsible for ignoring warnings as desired, and for formatting the warning messages.
2471 You can override the value of `ignored_warning_kinds` in a subclass of GenerateJsonSchema
2472 to modify what warnings are generated. If you want more control, you can override this method;
2473 just return None in situations where you don't want warnings to be emitted.
2475 Args:
2476 kind: The kind of warning to render. It can be one of the following:
2478 - 'skipped-choice': A choice field was skipped because it had no valid choices.
2479 - 'non-serializable-default': A default value was skipped because it was not JSON-serializable.
2480 detail: A string with additional details about the warning.
2482 Returns:
2483 The formatted warning message, or `None` if no warning should be emitted.
2484 """
2485 if kind in self.ignored_warning_kinds:
2486 return None
2487 return f'{detail} [{kind}]'
2489 def _build_definitions_remapping(self) -> _DefinitionsRemapping:
2490 defs_to_json: dict[DefsRef, JsonRef] = {}
2491 for defs_refs in self._prioritized_defsref_choices.values():
2492 for defs_ref in defs_refs:
2493 json_ref = JsonRef(self.ref_template.format(model=defs_ref))
2494 defs_to_json[defs_ref] = json_ref
2496 return _DefinitionsRemapping.from_prioritized_choices(
2497 self._prioritized_defsref_choices, defs_to_json, self.definitions
2498 )
2500 def _garbage_collect_definitions(self, schema: JsonSchemaValue) -> None:
2501 visited_defs_refs: set[DefsRef] = set()
2502 unvisited_json_refs = _get_all_json_refs(schema)
2503 while unvisited_json_refs:
2504 next_json_ref = unvisited_json_refs.pop()
2505 try:
2506 next_defs_ref = self.json_to_defs_refs[next_json_ref]
2507 if next_defs_ref in visited_defs_refs:
2508 continue
2509 visited_defs_refs.add(next_defs_ref)
2510 unvisited_json_refs.update(_get_all_json_refs(self.definitions[next_defs_ref]))
2511 except KeyError:
2512 if not next_json_ref.startswith(('http://', 'https://')):
2513 raise
2515 self.definitions = {k: v for k, v in self.definitions.items() if k in visited_defs_refs}
2518# ##### Start JSON Schema Generation Functions #####
2521def model_json_schema(
2522 cls: type[BaseModel] | type[PydanticDataclass],
2523 by_alias: bool = True,
2524 ref_template: str = DEFAULT_REF_TEMPLATE,
2525 union_format: Literal['any_of', 'primitive_type_array'] = 'any_of',
2526 schema_generator: type[GenerateJsonSchema] = GenerateJsonSchema,
2527 mode: JsonSchemaMode = 'validation',
2528) -> dict[str, Any]:
2529 """Utility function to generate a JSON Schema for a model.
2531 Args:
2532 cls: The model class to generate a JSON Schema for.
2533 by_alias: If `True` (the default), fields will be serialized according to their alias.
2534 If `False`, fields will be serialized according to their attribute name.
2535 ref_template: The template to use for generating JSON Schema references.
2536 union_format: The format to use when combining schemas from unions together. Can be one of:
2538 - `'any_of'`: Use the [`anyOf`](https://json-schema.org/understanding-json-schema/reference/combining#anyOf)
2539 keyword to combine schemas (the default).
2540 - `'primitive_type_array'`: Use the [`type`](https://json-schema.org/understanding-json-schema/reference/type)
2541 keyword as an array of strings, containing each type of the combination. If any of the schemas is not a primitive
2542 type (`string`, `boolean`, `null`, `integer` or `number`) or contains constraints/metadata, falls back to
2543 `any_of`.
2544 schema_generator: The class to use for generating the JSON Schema.
2545 mode: The mode to use for generating the JSON Schema. It can be one of the following:
2547 - 'validation': Generate a JSON Schema for validating data.
2548 - 'serialization': Generate a JSON Schema for serializing data.
2550 Returns:
2551 The generated JSON Schema.
2552 """
2553 from .main import BaseModel
2555 schema_generator_instance = schema_generator(
2556 by_alias=by_alias, ref_template=ref_template, union_format=union_format
2557 )
2559 if isinstance(cls.__pydantic_core_schema__, _mock_val_ser.MockCoreSchema):
2560 cls.__pydantic_core_schema__.rebuild()
2562 if cls is BaseModel:
2563 raise AttributeError('model_json_schema() must be called on a subclass of BaseModel, not BaseModel itself.')
2565 assert not isinstance(cls.__pydantic_core_schema__, _mock_val_ser.MockCoreSchema), 'this is a bug! please report it'
2566 return schema_generator_instance.generate(cls.__pydantic_core_schema__, mode=mode)
2569def models_json_schema(
2570 models: Sequence[tuple[type[BaseModel] | type[PydanticDataclass], JsonSchemaMode]],
2571 *,
2572 by_alias: bool = True,
2573 title: str | None = None,
2574 description: str | None = None,
2575 ref_template: str = DEFAULT_REF_TEMPLATE,
2576 union_format: Literal['any_of', 'primitive_type_array'] = 'any_of',
2577 schema_generator: type[GenerateJsonSchema] = GenerateJsonSchema,
2578) -> tuple[dict[tuple[type[BaseModel] | type[PydanticDataclass], JsonSchemaMode], JsonSchemaValue], JsonSchemaValue]:
2579 """Utility function to generate a JSON Schema for multiple models.
2581 Args:
2582 models: A sequence of tuples of the form (model, mode).
2583 by_alias: Whether field aliases should be used as keys in the generated JSON Schema.
2584 title: The title of the generated JSON Schema.
2585 description: The description of the generated JSON Schema.
2586 ref_template: The reference template to use for generating JSON Schema references.
2587 union_format: The format to use when combining schemas from unions together. Can be one of:
2589 - `'any_of'`: Use the [`anyOf`](https://json-schema.org/understanding-json-schema/reference/combining#anyOf)
2590 keyword to combine schemas (the default).
2591 - `'primitive_type_array'`: Use the [`type`](https://json-schema.org/understanding-json-schema/reference/type)
2592 keyword as an array of strings, containing each type of the combination. If any of the schemas is not a primitive
2593 type (`string`, `boolean`, `null`, `integer` or `number`) or contains constraints/metadata, falls back to
2594 `any_of`.
2595 schema_generator: The schema generator to use for generating the JSON Schema.
2597 Returns:
2598 A tuple where:
2599 - The first element is a dictionary whose keys are tuples of JSON schema key type and JSON mode, and
2600 whose values are the JSON schema corresponding to that pair of inputs. (These schemas may have
2601 JsonRef references to definitions that are defined in the second returned element.)
2602 - The second element is a JSON schema containing all definitions referenced in the first returned
2603 element, along with the optional title and description keys.
2604 """
2605 for cls, _ in models:
2606 if isinstance(cls.__pydantic_core_schema__, _mock_val_ser.MockCoreSchema):
2607 cls.__pydantic_core_schema__.rebuild()
2609 instance = schema_generator(by_alias=by_alias, ref_template=ref_template, union_format=union_format)
2610 inputs: list[tuple[type[BaseModel] | type[PydanticDataclass], JsonSchemaMode, CoreSchema]] = [
2611 (m, mode, m.__pydantic_core_schema__) for m, mode in models
2612 ]
2613 json_schemas_map, definitions = instance.generate_definitions(inputs)
2615 json_schema: dict[str, Any] = {}
2616 if definitions:
2617 json_schema['$defs'] = definitions
2618 if title:
2619 json_schema['title'] = title
2620 if description:
2621 json_schema['description'] = description
2623 return json_schemas_map, json_schema
2626# ##### End JSON Schema Generation Functions #####
2629_HashableJsonValue: TypeAlias = Union[
2630 int, float, str, bool, None, tuple['_HashableJsonValue', ...], tuple[tuple[str, '_HashableJsonValue'], ...]
2631]
2634def _deduplicate_schemas(schemas: Iterable[JsonDict]) -> list[JsonDict]:
2635 return list({_make_json_hashable(schema): schema for schema in schemas}.values())
2638def _make_json_hashable(value: JsonValue) -> _HashableJsonValue:
2639 if isinstance(value, dict):
2640 return tuple(sorted((k, _make_json_hashable(v)) for k, v in value.items()))
2641 elif isinstance(value, list):
2642 return tuple(_make_json_hashable(v) for v in value)
2643 else:
2644 return value
2647@dataclasses.dataclass(**_internal_dataclass.slots_true)
2648class WithJsonSchema:
2649 """!!! abstract "Usage Documentation"
2650 [`WithJsonSchema` Annotation](../concepts/json_schema.md#withjsonschema-annotation)
2652 An annotation used to override the JSON Schema for a type.
2654 This is useful when you want to set a JSON Schema for a type that don't produce any JSON Schemas by default
2655 (e.g. [`Callable`][collections.abc.Callable]).
2657 If `mode` is set this will only apply to that schema generation mode, allowing you to set different JSON Schemas for validation and serialization.
2659 !!! note
2660 If the `WithJsonSchema` annotation is coupled with the [`Field()`][pydantic.Field] function, the behavior overriding will vary depending on the location:
2662 * If the [`Annotated`][typing.Annotated] metadata is specified at the "top-level" field, `Field()` metadata arguments
2663 (excluding [constraints](../concepts/fields.md#field-constraints)) such as `title` and `description` will be applied on
2664 top of the `WithJsonSchema`, no matter the order:
2666 ```python
2667 from typing import Annotated
2669 from pydantic import BaseModel, Field, WithJsonSchema
2671 class Model(BaseModel):
2672 field: Annotated[
2673 int,
2674 Field(title='My Field'),
2675 WithJsonSchema({'type': 'integer', 'extra': 'data'}),
2676 ]
2678 Model.model_json_schema()['properties']['field']
2679 #> {'type': 'integer', 'extra': 'data', 'title': 'My Field'}
2680 ```
2682 * If the [`Annotated`][typing.Annotated] metadata is specified on a specific inner type, `WithJsonSchema` will unconditionally
2683 override the JSON Schema:
2685 ```python
2686 from typing import Annotated
2688 from pydantic import BaseModel, Field, WithJsonSchema
2690 class Model(BaseModel):
2691 field: list[
2692 Annotated[
2693 int,
2694 Field(title='My Field'),
2695 WithJsonSchema({'type': 'integer', 'extra': 'data'}),
2696 ]
2697 ]
2699 Model.model_json_schema()['properties']['field']
2700 #> {'items': {'extra': 'data', 'type': 'integer'}, 'title': 'Field', 'type': 'array'}
2701 ```
2703 See also the documentation about [the annotated pattern](../concepts/fields.md#the-annotated-pattern).
2704 """
2706 json_schema: JsonSchemaValue | None
2707 mode: Literal['validation', 'serialization'] | None = None
2709 def __get_pydantic_json_schema__(
2710 self, core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler
2711 ) -> JsonSchemaValue:
2712 if self.mode is not None and self.mode != handler.mode:
2713 return handler(core_schema)
2714 if self.json_schema is None:
2715 # This exception is handled in pydantic.json_schema.GenerateJsonSchema._named_required_fields_schema
2716 raise PydanticOmit
2717 else:
2718 return self.json_schema.copy()
2720 def __hash__(self) -> int:
2721 return hash(type(self.mode))
2724class Examples:
2725 """Add examples to a JSON schema.
2727 If the JSON Schema already contains examples, the provided examples
2728 will be appended.
2730 If `mode` is set this will only apply to that schema generation mode,
2731 allowing you to add different examples for validation and serialization.
2732 """
2734 @overload
2735 @deprecated('Using a dict for `examples` is deprecated since v2.9 and will be removed in v3.0. Use a list instead.')
2736 def __init__(
2737 self, examples: dict[str, Any], mode: Literal['validation', 'serialization'] | None = None
2738 ) -> None: ...
2740 @overload
2741 def __init__(self, examples: list[Any], mode: Literal['validation', 'serialization'] | None = None) -> None: ...
2743 def __init__(
2744 self, examples: dict[str, Any] | list[Any], mode: Literal['validation', 'serialization'] | None = None
2745 ) -> None:
2746 if isinstance(examples, dict):
2747 warnings.warn(
2748 'Using a dict for `examples` is deprecated, use a list instead.',
2749 PydanticDeprecatedSince29,
2750 stacklevel=2,
2751 )
2752 self.examples = examples
2753 self.mode = mode
2755 def __get_pydantic_json_schema__(
2756 self, core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler
2757 ) -> JsonSchemaValue:
2758 mode = self.mode or handler.mode
2759 json_schema = handler(core_schema)
2760 if mode != handler.mode:
2761 return json_schema
2762 examples = json_schema.get('examples')
2763 if examples is None:
2764 json_schema['examples'] = to_jsonable_python(self.examples)
2765 if isinstance(examples, dict):
2766 if isinstance(self.examples, list):
2767 warnings.warn(
2768 'Updating existing JSON Schema examples of type dict with examples of type list. '
2769 'Only the existing examples values will be retained. Note that dict support for '
2770 'examples is deprecated and will be removed in v3.0.',
2771 UserWarning,
2772 )
2773 json_schema['examples'] = to_jsonable_python(
2774 [ex for value in examples.values() for ex in value] + self.examples
2775 )
2776 else:
2777 json_schema['examples'] = to_jsonable_python({**examples, **self.examples})
2778 if isinstance(examples, list):
2779 if isinstance(self.examples, list):
2780 json_schema['examples'] = to_jsonable_python(examples + self.examples)
2781 elif isinstance(self.examples, dict):
2782 warnings.warn(
2783 'Updating existing JSON Schema examples of type list with examples of type dict. '
2784 'Only the examples values will be retained. Note that dict support for '
2785 'examples is deprecated and will be removed in v3.0.',
2786 UserWarning,
2787 )
2788 json_schema['examples'] = to_jsonable_python(
2789 examples + [ex for value in self.examples.values() for ex in value]
2790 )
2792 return json_schema
2794 def __hash__(self) -> int:
2795 return hash(type(self.mode))
2798def _get_all_json_refs(item: Any) -> set[JsonRef]:
2799 """Get all the definitions references from a JSON schema."""
2800 refs: set[JsonRef] = set()
2801 stack = [item]
2803 while stack:
2804 current = stack.pop()
2805 if isinstance(current, dict):
2806 for key, value in current.items():
2807 if key == 'examples' and isinstance(value, list):
2808 # Skip examples that may contain arbitrary values and references
2809 # (e.g. `{"examples": [{"$ref": "..."}]}`). Note: checking for value
2810 # of type list is necessary to avoid skipping valid portions of the schema,
2811 # for instance when "examples" is used as a property key. A more robust solution
2812 # could be found, but would require more advanced JSON Schema parsing logic.
2813 continue
2814 if key == '$ref' and isinstance(value, str):
2815 refs.add(JsonRef(value))
2816 elif isinstance(value, dict):
2817 stack.append(value)
2818 elif isinstance(value, list):
2819 stack.extend(value)
2820 elif isinstance(current, list):
2821 stack.extend(current)
2823 return refs
2826AnyType = TypeVar('AnyType')
2828if TYPE_CHECKING:
2829 SkipJsonSchema = Annotated[AnyType, ...]
2830else:
2832 @dataclasses.dataclass(**_internal_dataclass.slots_true)
2833 class SkipJsonSchema:
2834 """!!! abstract "Usage Documentation"
2835 [`SkipJsonSchema` Annotation](../concepts/json_schema.md#skipjsonschema-annotation)
2837 Add this as an annotation on a field to skip generating a JSON schema for that field.
2839 Example:
2840 ```python
2841 from pprint import pprint
2842 from typing import Union
2844 from pydantic import BaseModel
2845 from pydantic.json_schema import SkipJsonSchema
2847 class Model(BaseModel):
2848 a: Union[int, None] = None # (1)!
2849 b: Union[int, SkipJsonSchema[None]] = None # (2)!
2850 c: SkipJsonSchema[Union[int, None]] = None # (3)!
2852 pprint(Model.model_json_schema())
2853 '''
2854 {
2855 'properties': {
2856 'a': {
2857 'anyOf': [
2858 {'type': 'integer'},
2859 {'type': 'null'}
2860 ],
2861 'default': None,
2862 'title': 'A'
2863 },
2864 'b': {
2865 'default': None,
2866 'title': 'B',
2867 'type': 'integer'
2868 }
2869 },
2870 'title': 'Model',
2871 'type': 'object'
2872 }
2873 '''
2874 ```
2876 1. The integer and null types are both included in the schema for `a`.
2877 2. The integer type is the only type included in the schema for `b`.
2878 3. The entirety of the `c` field is omitted from the schema.
2879 """
2881 def __class_getitem__(cls, item: AnyType) -> AnyType:
2882 return Annotated[item, cls()]
2884 def __get_pydantic_json_schema__(
2885 self, core_schema: CoreSchema, handler: GetJsonSchemaHandler
2886 ) -> JsonSchemaValue:
2887 raise PydanticOmit
2889 def __hash__(self) -> int:
2890 return hash(type(self))
2893def _get_typed_dict_config(cls: type[Any] | None) -> ConfigDict:
2894 if cls is not None:
2895 try:
2896 return _decorators.get_attribute_from_bases(cls, '__pydantic_config__')
2897 except AttributeError:
2898 pass
2899 return {}
2902def _get_ser_schema_for_default_value(schema: CoreSchema) -> core_schema.PlainSerializerFunctionSerSchema | None:
2903 """Get a `'function-plain'` serialization schema that can be used to serialize a default value.
2905 This takes into account having the serialization schema nested under validation schema(s).
2906 """
2907 if (
2908 (ser_schema := schema.get('serialization'))
2909 and ser_schema['type'] == 'function-plain'
2910 and not ser_schema.get('info_arg')
2911 ):
2912 return ser_schema
2913 if _core_utils.is_function_with_inner_schema(schema):
2914 return _get_ser_schema_for_default_value(schema['schema'])