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 dataclasses
15import inspect
16import math
17import os
18import re
19import warnings
20from collections import Counter, defaultdict
21from collections.abc import Hashable, Iterable, Sequence
22from copy import deepcopy
23from enum import Enum
24from re import Pattern
25from typing import (
26 TYPE_CHECKING,
27 Annotated,
28 Any,
29 Callable,
30 Literal,
31 NewType,
32 TypeVar,
33 Union,
34 cast,
35 overload,
36)
38import pydantic_core
39from pydantic_core import MISSING, CoreSchema, PydanticOmit, core_schema, to_jsonable_python
40from pydantic_core.core_schema import ComputedField
41from typing_extensions import TypeAlias, assert_never, deprecated, final
42from typing_inspection.introspection import get_literal_values
44from pydantic.warnings import PydanticDeprecatedSince26, PydanticDeprecatedSince29
46from ._internal import (
47 _config,
48 _core_metadata,
49 _core_utils,
50 _decorators,
51 _internal_dataclass,
52 _mock_val_ser,
53 _schema_generation_shared,
54)
55from .annotated_handlers import GetJsonSchemaHandler
56from .config import JsonDict, JsonValue
57from .errors import PydanticInvalidForJsonSchema, PydanticSchemaGenerationError, PydanticUserError
59if TYPE_CHECKING:
60 from . import ConfigDict
61 from ._internal._core_utils import CoreSchemaField, CoreSchemaOrField
62 from ._internal._dataclasses import PydanticDataclass
63 from ._internal._schema_generation_shared import GetJsonSchemaFunction
64 from .main import BaseModel
67CoreSchemaOrFieldType = Literal[core_schema.CoreSchemaType, core_schema.CoreSchemaFieldType]
68"""
69A type alias for defined schema types that represents a union of
70`core_schema.CoreSchemaType` and
71`core_schema.CoreSchemaFieldType`.
72"""
74JsonSchemaValue = dict[str, Any]
75"""
76A type alias for a JSON schema value. This is a dictionary of string keys to arbitrary JSON values.
77"""
79JsonSchemaMode = Literal['validation', 'serialization']
80"""
81A type alias that represents the mode of a JSON schema; either 'validation' or 'serialization'.
83For some types, the inputs to validation differ from the outputs of serialization. For example,
84computed fields will only be present when serializing, and should not be provided when
85validating. This flag provides a way to indicate whether you want the JSON schema required
86for validation inputs, or that will be matched by serialization outputs.
87"""
89_MODE_TITLE_MAPPING: dict[JsonSchemaMode, str] = {'validation': 'Input', 'serialization': 'Output'}
92JsonSchemaWarningKind = Literal['skipped-choice', 'non-serializable-default', 'skipped-discriminator']
93"""
94A type alias representing the kinds of warnings that can be emitted during JSON schema generation.
96See [`GenerateJsonSchema.render_warning_message`][pydantic.json_schema.GenerateJsonSchema.render_warning_message]
97for more details.
98"""
101class PydanticJsonSchemaWarning(UserWarning):
102 """This class is used to emit warnings produced during JSON schema generation.
103 See the [`GenerateJsonSchema.emit_warning`][pydantic.json_schema.GenerateJsonSchema.emit_warning] and
104 [`GenerateJsonSchema.render_warning_message`][pydantic.json_schema.GenerateJsonSchema.render_warning_message]
105 methods for more details; these can be overridden to control warning behavior.
106 """
109NoDefault = object()
110"""A sentinel value used to indicate that no default value should be used when generating a JSON Schema
111for a core schema with a default value.
112"""
115# ##### JSON Schema Generation #####
116DEFAULT_REF_TEMPLATE = '#/$defs/{model}'
117"""The default format string used to generate reference names."""
119# There are three types of references relevant to building JSON schemas:
120# 1. core_schema "ref" values; these are not exposed as part of the JSON schema
121# * these might look like the fully qualified path of a model, its id, or something similar
122CoreRef = NewType('CoreRef', str)
123# 2. keys of the "definitions" object that will eventually go into the JSON schema
124# * by default, these look like "MyModel", though may change in the presence of collisions
125# * eventually, we may want to make it easier to modify the way these names are generated
126DefsRef = NewType('DefsRef', str)
127# 3. the values corresponding to the "$ref" key in the schema
128# * By default, these look like "#/$defs/MyModel", as in {"$ref": "#/$defs/MyModel"}
129JsonRef = NewType('JsonRef', str)
131CoreModeRef = tuple[CoreRef, JsonSchemaMode]
132JsonSchemaKeyT = TypeVar('JsonSchemaKeyT', bound=Hashable)
134_PRIMITIVE_JSON_SCHEMA_TYPES = ('string', 'boolean', 'null', 'integer', 'number')
137@dataclasses.dataclass(**_internal_dataclass.slots_true)
138class _DefinitionsRemapping:
139 defs_remapping: dict[DefsRef, DefsRef]
140 json_remapping: dict[JsonRef, JsonRef]
142 @staticmethod
143 def from_prioritized_choices(
144 prioritized_choices: dict[DefsRef, list[DefsRef]],
145 defs_to_json: dict[DefsRef, JsonRef],
146 definitions: dict[DefsRef, JsonSchemaValue],
147 ) -> _DefinitionsRemapping:
148 """
149 This function should produce a remapping that replaces complex DefsRef with the simpler ones from the
150 prioritized_choices such that applying the name remapping would result in an equivalent JSON schema.
151 """
152 # We need to iteratively simplify the definitions until we reach a fixed point.
153 # The reason for this is that outer definitions may reference inner definitions that get simplified
154 # into an equivalent reference, and the outer definitions won't be equivalent until we've simplified
155 # the inner definitions.
156 copied_definitions = deepcopy(definitions)
157 definitions_schema = {'$defs': copied_definitions}
158 for _iter in range(100): # prevent an infinite loop in the case of a bug, 100 iterations should be enough
159 # For every possible remapped DefsRef, collect all schemas that that DefsRef might be used for:
160 schemas_for_alternatives: dict[DefsRef, list[JsonSchemaValue]] = defaultdict(list)
161 for defs_ref in copied_definitions:
162 alternatives = prioritized_choices[defs_ref]
163 for alternative in alternatives:
164 schemas_for_alternatives[alternative].append(copied_definitions[defs_ref])
166 # Deduplicate the schemas for each alternative; the idea is that we only want to remap to a new DefsRef
167 # if it introduces no ambiguity, i.e., there is only one distinct schema for that DefsRef.
168 for defs_ref in schemas_for_alternatives:
169 schemas_for_alternatives[defs_ref] = _deduplicate_schemas(schemas_for_alternatives[defs_ref])
171 # Build the remapping
172 defs_remapping: dict[DefsRef, DefsRef] = {}
173 json_remapping: dict[JsonRef, JsonRef] = {}
174 for original_defs_ref in definitions:
175 alternatives = prioritized_choices[original_defs_ref]
176 # Pick the first alternative that has only one schema, since that means there is no collision
177 remapped_defs_ref = next(x for x in alternatives if len(schemas_for_alternatives[x]) == 1)
178 defs_remapping[original_defs_ref] = remapped_defs_ref
179 json_remapping[defs_to_json[original_defs_ref]] = defs_to_json[remapped_defs_ref]
180 remapping = _DefinitionsRemapping(defs_remapping, json_remapping)
181 new_definitions_schema = remapping.remap_json_schema({'$defs': copied_definitions})
182 if definitions_schema == new_definitions_schema:
183 # We've reached the fixed point
184 return remapping
185 definitions_schema = new_definitions_schema
187 raise PydanticInvalidForJsonSchema('Failed to simplify the JSON schema definitions')
189 def remap_defs_ref(self, ref: DefsRef) -> DefsRef:
190 return self.defs_remapping.get(ref, ref)
192 def remap_json_ref(self, ref: JsonRef) -> JsonRef:
193 return self.json_remapping.get(ref, ref)
195 def remap_json_schema(self, schema: Any) -> Any:
196 """
197 Recursively update the JSON schema replacing all $refs
198 """
199 if isinstance(schema, str):
200 # Note: this may not really be a JsonRef; we rely on having no collisions between JsonRefs and other strings
201 return self.remap_json_ref(JsonRef(schema))
202 elif isinstance(schema, list):
203 return [self.remap_json_schema(item) for item in schema]
204 elif isinstance(schema, dict):
205 for key, value in schema.items():
206 if key == '$ref' and isinstance(value, str):
207 schema['$ref'] = self.remap_json_ref(JsonRef(value))
208 elif key == '$defs':
209 schema['$defs'] = {
210 self.remap_defs_ref(DefsRef(key)): self.remap_json_schema(value)
211 for key, value in schema['$defs'].items()
212 }
213 else:
214 schema[key] = self.remap_json_schema(value)
215 return schema
218class GenerateJsonSchema:
219 """!!! abstract "Usage Documentation"
220 [Customizing the JSON Schema Generation Process](../concepts/json_schema.md#customizing-the-json-schema-generation-process)
222 A class for generating JSON schemas.
224 This class generates JSON schemas based on configured parameters. The default schema dialect
225 is [https://json-schema.org/draft/2020-12/schema](https://json-schema.org/draft/2020-12/schema).
226 The class uses `by_alias` to configure how fields with
227 multiple names are handled and `ref_template` to format reference names.
229 Attributes:
230 schema_dialect: The JSON schema dialect used to generate the schema. See
231 [Declaring a Dialect](https://json-schema.org/understanding-json-schema/reference/schema.html#id4)
232 in the JSON Schema documentation for more information about dialects.
233 ignored_warning_kinds: Warnings to ignore when generating the schema. `self.render_warning_message` will
234 do nothing if its argument `kind` is in `ignored_warning_kinds`;
235 this value can be modified on subclasses to easily control which warnings are emitted.
236 by_alias: Whether to use field aliases when generating the schema.
237 ref_template: The format string used when generating reference names.
238 core_to_json_refs: A mapping of core refs to JSON refs.
239 core_to_defs_refs: A mapping of core refs to definition refs.
240 defs_to_core_refs: A mapping of definition refs to core refs.
241 json_to_defs_refs: A mapping of JSON refs to definition refs.
242 definitions: Definitions in the schema.
244 Args:
245 by_alias: Whether to use field aliases in the generated schemas.
246 ref_template: The format string to use when generating reference names.
247 union_format: The format to use when combining schemas from unions together. Can be one of:
249 - `'any_of'`: Use the [`anyOf`](https://json-schema.org/understanding-json-schema/reference/combining#anyOf)
250 keyword to combine schemas (the default).
251 - `'primitive_type_array'`: Use the [`type`](https://json-schema.org/understanding-json-schema/reference/type)
252 keyword as an array of strings, containing each type of the combination. If any of the schemas is not a primitive
253 type (`string`, `boolean`, `null`, `integer` or `number`) or contains constraints/metadata, falls back to
254 `any_of`.
256 Raises:
257 JsonSchemaError: If the instance of the class is inadvertently reused after generating a schema.
258 """
260 schema_dialect = 'https://json-schema.org/draft/2020-12/schema'
262 # `self.render_warning_message` will do nothing if its argument `kind` is in `ignored_warning_kinds`;
263 # this value can be modified on subclasses to easily control which warnings are emitted
264 ignored_warning_kinds: set[JsonSchemaWarningKind] = {'skipped-choice'}
266 def __init__(
267 self,
268 by_alias: bool = True,
269 ref_template: str = DEFAULT_REF_TEMPLATE,
270 union_format: Literal['any_of', 'primitive_type_array'] = 'any_of',
271 ) -> None:
272 self.by_alias = by_alias
273 self.ref_template = ref_template
274 self.union_format: Literal['any_of', 'primitive_type_array'] = union_format
276 self.core_to_json_refs: dict[CoreModeRef, JsonRef] = {}
277 self.core_to_defs_refs: dict[CoreModeRef, DefsRef] = {}
278 self.defs_to_core_refs: dict[DefsRef, CoreModeRef] = {}
279 self.json_to_defs_refs: dict[JsonRef, DefsRef] = {}
281 self.definitions: dict[DefsRef, JsonSchemaValue] = {}
282 self._config_wrapper_stack = _config.ConfigWrapperStack(_config.ConfigWrapper({}))
284 self._mode: JsonSchemaMode = 'validation'
286 # The following includes a mapping of a fully-unique defs ref choice to a list of preferred
287 # alternatives, which are generally simpler, such as only including the class name.
288 # At the end of schema generation, we use these to produce a JSON schema with more human-readable
289 # definitions, which would also work better in a generated OpenAPI client, etc.
290 self._prioritized_defsref_choices: dict[DefsRef, list[DefsRef]] = {}
291 self._collision_counter: dict[str, int] = defaultdict(int)
292 self._collision_index: dict[str, int] = {}
294 self._schema_type_to_method = self.build_schema_type_to_method()
296 # When we encounter definitions we need to try to build them immediately
297 # so that they are available schemas that reference them
298 # But it's possible that CoreSchema was never going to be used
299 # (e.g. because the CoreSchema that references short circuits is JSON schema generation without needing
300 # the reference) so instead of failing altogether if we can't build a definition we
301 # store the error raised and re-throw it if we end up needing that def
302 self._core_defs_invalid_for_json_schema: dict[DefsRef, PydanticInvalidForJsonSchema] = {}
304 # This changes to True after generating a schema, to prevent issues caused by accidental reuse
305 # of a single instance of a schema generator
306 self._used = False
308 @property
309 def _config(self) -> _config.ConfigWrapper:
310 return self._config_wrapper_stack.tail
312 @property
313 def mode(self) -> JsonSchemaMode:
314 if self._config.json_schema_mode_override is not None:
315 return self._config.json_schema_mode_override
316 else:
317 return self._mode
319 def build_schema_type_to_method(
320 self,
321 ) -> dict[CoreSchemaOrFieldType, Callable[[CoreSchemaOrField], JsonSchemaValue]]:
322 """Builds a dictionary mapping fields to methods for generating JSON schemas.
324 Returns:
325 A dictionary containing the mapping of `CoreSchemaOrFieldType` to a handler method.
327 Raises:
328 TypeError: If no method has been defined for generating a JSON schema for a given pydantic core schema type.
329 """
330 mapping: dict[CoreSchemaOrFieldType, Callable[[CoreSchemaOrField], JsonSchemaValue]] = {}
331 core_schema_types: list[CoreSchemaOrFieldType] = list(get_literal_values(CoreSchemaOrFieldType))
332 for key in core_schema_types:
333 method_name = f'{key.replace("-", "_")}_schema'
334 try:
335 mapping[key] = getattr(self, method_name)
336 except AttributeError as e: # pragma: no cover
337 if os.getenv('PYDANTIC_PRIVATE_ALLOW_UNHANDLED_SCHEMA_TYPES'):
338 continue
339 raise TypeError(
340 f'No method for generating JsonSchema for core_schema.type={key!r} '
341 f'(expected: {type(self).__name__}.{method_name})'
342 ) from e
343 return mapping
345 def generate_definitions(
346 self, inputs: Sequence[tuple[JsonSchemaKeyT, JsonSchemaMode, core_schema.CoreSchema]]
347 ) -> tuple[dict[tuple[JsonSchemaKeyT, JsonSchemaMode], JsonSchemaValue], dict[DefsRef, JsonSchemaValue]]:
348 """Generates JSON schema definitions from a list of core schemas, pairing the generated definitions with a
349 mapping that links the input keys to the definition references.
351 Args:
352 inputs: A sequence of tuples, where:
354 - The first element is a JSON schema key type.
355 - The second element is the JSON mode: either 'validation' or 'serialization'.
356 - The third element is a core schema.
358 Returns:
359 A tuple where:
361 - The first element is a dictionary whose keys are tuples of JSON schema key type and JSON mode, and
362 whose values are the JSON schema corresponding to that pair of inputs. (These schemas may have
363 JsonRef references to definitions that are defined in the second returned element.)
364 - The second element is a dictionary whose keys are definition references for the JSON schemas
365 from the first returned element, and whose values are the actual JSON schema definitions.
367 Raises:
368 PydanticUserError: Raised if the JSON schema generator has already been used to generate a JSON schema.
369 """
370 if self._used:
371 raise PydanticUserError(
372 'This JSON schema generator has already been used to generate a JSON schema. '
373 f'You must create a new instance of {type(self).__name__} to generate a new JSON schema.',
374 code='json-schema-already-used',
375 )
377 for _, mode, schema in inputs:
378 self._mode = mode
379 self.generate_inner(schema)
381 definitions_remapping = self._build_definitions_remapping()
383 json_schemas_map: dict[tuple[JsonSchemaKeyT, JsonSchemaMode], DefsRef] = {}
384 for key, mode, schema in inputs:
385 self._mode = mode
386 json_schema = self.generate_inner(schema)
387 json_schemas_map[(key, mode)] = definitions_remapping.remap_json_schema(json_schema)
389 json_schema = {'$defs': self.definitions}
390 json_schema = definitions_remapping.remap_json_schema(json_schema)
391 self._used = True
392 return json_schemas_map, self.sort(json_schema['$defs']) # type: ignore
394 def generate(self, schema: CoreSchema, mode: JsonSchemaMode = 'validation') -> JsonSchemaValue:
395 """Generates a JSON schema for a specified schema in a specified mode.
397 Args:
398 schema: A Pydantic model.
399 mode: The mode in which to generate the schema. Defaults to 'validation'.
401 Returns:
402 A JSON schema representing the specified schema.
404 Raises:
405 PydanticUserError: If the JSON schema generator has already been used to generate a JSON schema.
406 """
407 self._mode = mode
408 if self._used:
409 raise PydanticUserError(
410 'This JSON schema generator has already been used to generate a JSON schema. '
411 f'You must create a new instance of {type(self).__name__} to generate a new JSON schema.',
412 code='json-schema-already-used',
413 )
415 json_schema: JsonSchemaValue = self.generate_inner(schema)
416 json_ref_counts = self.get_json_ref_counts(json_schema)
418 ref = cast(JsonRef, json_schema.get('$ref'))
419 while ref is not None: # may need to unpack multiple levels
420 ref_json_schema = self.get_schema_from_definitions(ref)
421 if json_ref_counts[ref] == 1 and ref_json_schema is not None and len(json_schema) == 1:
422 # "Unpack" the ref since this is the only reference and there are no sibling keys
423 json_schema = ref_json_schema.copy() # copy to prevent recursive dict reference
424 json_ref_counts[ref] -= 1
425 ref = cast(JsonRef, json_schema.get('$ref'))
426 ref = None
428 self._garbage_collect_definitions(json_schema)
429 definitions_remapping = self._build_definitions_remapping()
431 if self.definitions:
432 json_schema['$defs'] = self.definitions
434 json_schema = definitions_remapping.remap_json_schema(json_schema)
436 # For now, we will not set the $schema key. However, if desired, this can be easily added by overriding
437 # this method and adding the following line after a call to super().generate(schema):
438 # json_schema['$schema'] = self.schema_dialect
440 self._used = True
441 return self.sort(json_schema)
443 def generate_inner(self, schema: CoreSchemaOrField) -> JsonSchemaValue: # noqa: C901
444 """Generates a JSON schema for a given core schema.
446 Args:
447 schema: The given core schema.
449 Returns:
450 The generated JSON schema.
452 TODO: the nested function definitions here seem like bad practice, I'd like to unpack these
453 in a future PR. It'd be great if we could shorten the call stack a bit for JSON schema generation,
454 and I think there's potential for that here.
455 """
456 # If a schema with the same CoreRef has been handled, just return a reference to it
457 # Note that this assumes that it will _never_ be the case that the same CoreRef is used
458 # on types that should have different JSON schemas
459 if 'ref' in schema:
460 core_ref = CoreRef(schema['ref']) # type: ignore[typeddict-item]
461 core_mode_ref = (core_ref, self.mode)
462 if core_mode_ref in self.core_to_defs_refs and self.core_to_defs_refs[core_mode_ref] in self.definitions:
463 return {'$ref': self.core_to_json_refs[core_mode_ref]}
465 def populate_defs(core_schema: CoreSchema, json_schema: JsonSchemaValue) -> JsonSchemaValue:
466 if 'ref' in core_schema:
467 core_ref = CoreRef(core_schema['ref']) # type: ignore[typeddict-item]
468 defs_ref, ref_json_schema = self.get_cache_defs_ref_schema(core_ref)
469 json_ref = JsonRef(ref_json_schema['$ref'])
470 # Replace the schema if it's not a reference to itself
471 # What we want to avoid is having the def be just a ref to itself
472 # which is what would happen if we blindly assigned any
473 if json_schema.get('$ref', None) != json_ref:
474 self.definitions[defs_ref] = json_schema
475 self._core_defs_invalid_for_json_schema.pop(defs_ref, None)
476 json_schema = ref_json_schema
477 return json_schema
479 def handler_func(schema_or_field: CoreSchemaOrField) -> JsonSchemaValue:
480 """Generate a JSON schema based on the input schema.
482 Args:
483 schema_or_field: The core schema to generate a JSON schema from.
485 Returns:
486 The generated JSON schema.
488 Raises:
489 TypeError: If an unexpected schema type is encountered.
490 """
491 # Generate the core-schema-type-specific bits of the schema generation:
492 json_schema: JsonSchemaValue | None = None
493 if self.mode == 'serialization' and 'serialization' in schema_or_field:
494 # In this case, we skip the JSON Schema generation of the schema
495 # and use the `'serialization'` schema instead (canonical example:
496 # `Annotated[int, PlainSerializer(str)]`).
497 ser_schema = schema_or_field['serialization'] # type: ignore
498 json_schema = self.ser_schema(ser_schema)
500 # It might be that the 'serialization'` is skipped depending on `when_used`.
501 # This is only relevant for `nullable` schemas though, so we special case here.
502 if (
503 json_schema is not None
504 and ser_schema.get('when_used') in ('unless-none', 'json-unless-none')
505 and schema_or_field['type'] == 'nullable'
506 ):
507 json_schema = self.get_union_of_schemas([{'type': 'null'}, json_schema])
508 if json_schema is None:
509 if _core_utils.is_core_schema(schema_or_field) or _core_utils.is_core_schema_field(schema_or_field):
510 generate_for_schema_type = self._schema_type_to_method[schema_or_field['type']]
511 json_schema = generate_for_schema_type(schema_or_field)
512 else:
513 raise TypeError(f'Unexpected schema type: schema={schema_or_field}')
514 return json_schema
516 current_handler = _schema_generation_shared.GenerateJsonSchemaHandler(self, handler_func)
518 metadata = cast(_core_metadata.CoreMetadata, schema.get('metadata', {}))
520 # TODO: I dislike that we have to wrap these basic dict updates in callables, is there any way around this?
522 if js_updates := metadata.get('pydantic_js_updates'):
524 def js_updates_handler_func(
525 schema_or_field: CoreSchemaOrField,
526 current_handler: GetJsonSchemaHandler = current_handler,
527 ) -> JsonSchemaValue:
528 json_schema = {**current_handler(schema_or_field), **js_updates}
529 return json_schema
531 current_handler = _schema_generation_shared.GenerateJsonSchemaHandler(self, js_updates_handler_func)
533 if js_extra := metadata.get('pydantic_js_extra'):
535 def js_extra_handler_func(
536 schema_or_field: CoreSchemaOrField,
537 current_handler: GetJsonSchemaHandler = current_handler,
538 ) -> JsonSchemaValue:
539 json_schema = current_handler(schema_or_field)
540 if isinstance(js_extra, dict):
541 json_schema.update(to_jsonable_python(js_extra))
542 elif callable(js_extra):
543 # similar to typing issue in _update_class_schema when we're working with callable js extra
544 js_extra(json_schema) # type: ignore
545 return json_schema
547 current_handler = _schema_generation_shared.GenerateJsonSchemaHandler(self, js_extra_handler_func)
549 for js_modify_function in metadata.get('pydantic_js_functions', ()):
551 def new_handler_func(
552 schema_or_field: CoreSchemaOrField,
553 current_handler: GetJsonSchemaHandler = current_handler,
554 js_modify_function: GetJsonSchemaFunction = js_modify_function,
555 ) -> JsonSchemaValue:
556 json_schema = js_modify_function(schema_or_field, current_handler)
557 if _core_utils.is_core_schema(schema_or_field):
558 json_schema = populate_defs(schema_or_field, json_schema)
559 original_schema = current_handler.resolve_ref_schema(json_schema)
560 ref = json_schema.pop('$ref', None)
561 if ref and json_schema:
562 original_schema.update(json_schema)
563 return original_schema
565 current_handler = _schema_generation_shared.GenerateJsonSchemaHandler(self, new_handler_func)
567 for js_modify_function in metadata.get('pydantic_js_annotation_functions', ()):
569 def new_handler_func(
570 schema_or_field: CoreSchemaOrField,
571 current_handler: GetJsonSchemaHandler = current_handler,
572 js_modify_function: GetJsonSchemaFunction = js_modify_function,
573 ) -> JsonSchemaValue:
574 return js_modify_function(schema_or_field, current_handler)
576 current_handler = _schema_generation_shared.GenerateJsonSchemaHandler(self, new_handler_func)
578 json_schema = current_handler(schema)
579 if _core_utils.is_core_schema(schema):
580 json_schema = populate_defs(schema, json_schema)
581 return json_schema
583 def sort(self, value: JsonSchemaValue, parent_key: str | None = None) -> JsonSchemaValue:
584 """Override this method to customize the sorting of the JSON schema (e.g., don't sort at all, sort all keys unconditionally, etc.)
586 By default, alphabetically sort the keys in the JSON schema, skipping the 'properties' and 'default' keys to preserve field definition order.
587 This sort is recursive, so it will sort all nested dictionaries as well.
588 """
589 sorted_dict: dict[str, JsonSchemaValue] = {}
590 keys = value.keys()
591 if parent_key not in ('properties', 'default'):
592 keys = sorted(keys)
593 for key in keys:
594 sorted_dict[key] = self._sort_recursive(value[key], parent_key=key)
595 return sorted_dict
597 def _sort_recursive(self, value: Any, parent_key: str | None = None) -> Any:
598 """Recursively sort a JSON schema value."""
599 if isinstance(value, dict):
600 sorted_dict: dict[str, JsonSchemaValue] = {}
601 keys = value.keys()
602 if parent_key not in ('properties', 'default'):
603 keys = sorted(keys)
604 for key in keys:
605 sorted_dict[key] = self._sort_recursive(value[key], parent_key=key)
606 return sorted_dict
607 elif isinstance(value, list):
608 sorted_list: list[JsonSchemaValue] = [self._sort_recursive(item, parent_key) for item in value]
609 return sorted_list
610 else:
611 return value
613 # ### Schema generation methods
615 def invalid_schema(self, schema: core_schema.InvalidSchema) -> JsonSchemaValue:
616 """Placeholder - should never be called."""
618 raise RuntimeError('Cannot generate schema for invalid_schema. This is a bug! Please report it.')
620 def any_schema(self, schema: core_schema.AnySchema) -> JsonSchemaValue:
621 """Generates a JSON schema that matches any value.
623 Args:
624 schema: The core schema.
626 Returns:
627 The generated JSON schema.
628 """
629 return {}
631 def none_schema(self, schema: core_schema.NoneSchema) -> JsonSchemaValue:
632 """Generates a JSON schema that matches `None`.
634 Args:
635 schema: The core schema.
637 Returns:
638 The generated JSON schema.
639 """
640 return {'type': 'null'}
642 def bool_schema(self, schema: core_schema.BoolSchema) -> JsonSchemaValue:
643 """Generates a JSON schema that matches a bool value.
645 Args:
646 schema: The core schema.
648 Returns:
649 The generated JSON schema.
650 """
651 return {'type': 'boolean'}
653 def int_schema(self, schema: core_schema.IntSchema) -> JsonSchemaValue:
654 """Generates a JSON schema that matches an int value.
656 Args:
657 schema: The core schema.
659 Returns:
660 The generated JSON schema.
661 """
662 json_schema: dict[str, Any] = {'type': 'integer'}
663 self.update_with_validations(json_schema, schema, self.ValidationsMapping.numeric)
664 json_schema = {k: v for k, v in json_schema.items() if v not in {math.inf, -math.inf}}
665 return json_schema
667 def float_schema(self, schema: core_schema.FloatSchema) -> JsonSchemaValue:
668 """Generates a JSON schema that matches a float value.
670 Args:
671 schema: The core schema.
673 Returns:
674 The generated JSON schema.
675 """
676 json_schema: dict[str, Any] = {'type': 'number'}
677 self.update_with_validations(json_schema, schema, self.ValidationsMapping.numeric)
678 json_schema = {k: v for k, v in json_schema.items() if v not in {math.inf, -math.inf}}
679 return json_schema
681 def decimal_schema(self, schema: core_schema.DecimalSchema) -> JsonSchemaValue:
682 """Generates a JSON schema that matches a decimal value.
684 Args:
685 schema: The core schema.
687 Returns:
688 The generated JSON schema.
689 """
691 def get_decimal_pattern(schema: core_schema.DecimalSchema) -> str:
692 max_digits = schema.get('max_digits')
693 decimal_places = schema.get('decimal_places')
695 pattern = (
696 r'^(?!^[-+.]*$)[+-]?0*' # check it is not empty string and not one or sequence of ".+-" characters.
697 )
699 # Case 1: Both max_digits and decimal_places are set
700 if max_digits is not None and decimal_places is not None:
701 integer_places = max(0, max_digits - decimal_places)
702 pattern += (
703 rf'(?:'
704 rf'\d{{0,{integer_places}}}'
705 rf'|'
706 rf'(?=[\d.]{{1,{max_digits + 1}}}0*$)'
707 rf'\d{{0,{integer_places}}}\.\d{{0,{decimal_places}}}0*$'
708 rf')'
709 )
711 # Case 2: Only max_digits is set
712 elif max_digits is not None and decimal_places is None:
713 pattern += (
714 rf'(?:'
715 rf'\d{{0,{max_digits}}}'
716 rf'|'
717 rf'(?=[\d.]{{1,{max_digits + 1}}}0*$)'
718 rf'\d*\.\d*0*$'
719 rf')'
720 )
722 # Case 3: Only decimal_places is set
723 elif max_digits is None and decimal_places is not None:
724 pattern += rf'\d*\.?\d{{0,{decimal_places}}}0*$'
726 # Case 4: Both are None (no restrictions)
727 else:
728 pattern += r'\d*\.?\d*$' # look for arbitrary integer or decimal
730 return pattern
732 json_schema = self.str_schema(core_schema.str_schema(pattern=get_decimal_pattern(schema)))
733 if self.mode == 'validation':
734 multiple_of = schema.get('multiple_of')
735 le = schema.get('le')
736 ge = schema.get('ge')
737 lt = schema.get('lt')
738 gt = schema.get('gt')
739 json_schema = {
740 'anyOf': [
741 self.float_schema(
742 core_schema.float_schema(
743 allow_inf_nan=schema.get('allow_inf_nan'),
744 multiple_of=None if multiple_of is None else float(multiple_of),
745 le=None if le is None else float(le),
746 ge=None if ge is None else float(ge),
747 lt=None if lt is None else float(lt),
748 gt=None if gt is None else float(gt),
749 )
750 ),
751 json_schema,
752 ],
753 }
754 return json_schema
756 def str_schema(self, schema: core_schema.StringSchema) -> JsonSchemaValue:
757 """Generates a JSON schema that matches a string value.
759 Args:
760 schema: The core schema.
762 Returns:
763 The generated JSON schema.
764 """
765 json_schema = {'type': 'string'}
766 self.update_with_validations(json_schema, schema, self.ValidationsMapping.string)
767 if isinstance(json_schema.get('pattern'), Pattern):
768 # TODO: should we add regex flags to the pattern?
769 json_schema['pattern'] = json_schema.get('pattern').pattern # type: ignore
770 return json_schema
772 def bytes_schema(self, schema: core_schema.BytesSchema) -> JsonSchemaValue:
773 """Generates a JSON schema that matches a bytes value.
775 Args:
776 schema: The core schema.
778 Returns:
779 The generated JSON schema.
780 """
781 json_schema = {'type': 'string', 'format': 'base64url' if self._config.ser_json_bytes == 'base64' else 'binary'}
782 self.update_with_validations(json_schema, schema, self.ValidationsMapping.bytes)
783 return json_schema
785 def date_schema(self, schema: core_schema.DateSchema) -> JsonSchemaValue:
786 """Generates a JSON schema that matches a date value.
788 Args:
789 schema: The core schema.
791 Returns:
792 The generated JSON schema.
793 """
794 return {'type': 'string', 'format': 'date'}
796 def time_schema(self, schema: core_schema.TimeSchema) -> JsonSchemaValue:
797 """Generates a JSON schema that matches a time value.
799 Args:
800 schema: The core schema.
802 Returns:
803 The generated JSON schema.
804 """
805 return {'type': 'string', 'format': 'time'}
807 def datetime_schema(self, schema: core_schema.DatetimeSchema) -> JsonSchemaValue:
808 """Generates a JSON schema that matches a datetime value.
810 Args:
811 schema: The core schema.
813 Returns:
814 The generated JSON schema.
815 """
816 return {'type': 'string', 'format': 'date-time'}
818 def timedelta_schema(self, schema: core_schema.TimedeltaSchema) -> JsonSchemaValue:
819 """Generates a JSON schema that matches a timedelta value.
821 Args:
822 schema: The core schema.
824 Returns:
825 The generated JSON schema.
826 """
827 if self._config.ser_json_timedelta == 'float':
828 return {'type': 'number'}
829 return {'type': 'string', 'format': 'duration'}
831 def literal_schema(self, schema: core_schema.LiteralSchema) -> JsonSchemaValue:
832 """Generates a JSON schema that matches a literal value.
834 Args:
835 schema: The core schema.
837 Returns:
838 The generated JSON schema.
839 """
840 expected = [to_jsonable_python(v.value if isinstance(v, Enum) else v) for v in schema['expected']]
842 result: dict[str, Any] = {}
843 if len(expected) == 1:
844 result['const'] = expected[0]
845 else:
846 result['enum'] = expected
848 types = {type(e) for e in expected}
849 if types == {str}:
850 result['type'] = 'string'
851 elif types == {int}:
852 result['type'] = 'integer'
853 elif types == {float}:
854 result['type'] = 'number'
855 elif types == {bool}:
856 result['type'] = 'boolean'
857 elif types == {list}:
858 result['type'] = 'array'
859 elif types == {type(None)}:
860 result['type'] = 'null'
861 return result
863 def missing_sentinel_schema(self, schema: core_schema.MissingSentinelSchema) -> JsonSchemaValue:
864 """Generates a JSON schema that matches the `MISSING` sentinel value.
866 Args:
867 schema: The core schema.
869 Returns:
870 The generated JSON schema.
871 """
872 raise PydanticOmit
874 def enum_schema(self, schema: core_schema.EnumSchema) -> JsonSchemaValue:
875 """Generates a JSON schema that matches an Enum value.
877 Args:
878 schema: The core schema.
880 Returns:
881 The generated JSON schema.
882 """
883 enum_type = schema['cls']
884 description = None if not enum_type.__doc__ else inspect.cleandoc(enum_type.__doc__)
885 if (
886 description == 'An enumeration.'
887 ): # This is the default value provided by enum.EnumMeta.__new__; don't use it
888 description = None
889 result: dict[str, Any] = {'title': enum_type.__name__, 'description': description}
890 result = {k: v for k, v in result.items() if v is not None}
892 expected = [to_jsonable_python(v.value) for v in schema['members']]
894 result['enum'] = expected
896 types = {type(e) for e in expected}
897 if isinstance(enum_type, str) or types == {str}:
898 result['type'] = 'string'
899 elif isinstance(enum_type, int) or types == {int}:
900 result['type'] = 'integer'
901 elif isinstance(enum_type, float) or types == {float}:
902 result['type'] = 'number'
903 elif types == {bool}:
904 result['type'] = 'boolean'
905 elif types == {list}:
906 result['type'] = 'array'
908 return result
910 def is_instance_schema(self, schema: core_schema.IsInstanceSchema) -> JsonSchemaValue:
911 """Handles JSON schema generation for a core schema that checks if a value is an instance of a class.
913 Unless overridden in a subclass, this raises an error.
915 Args:
916 schema: The core schema.
918 Returns:
919 The generated JSON schema.
920 """
921 return self.handle_invalid_for_json_schema(schema, f'core_schema.IsInstanceSchema ({schema["cls"]})')
923 def is_subclass_schema(self, schema: core_schema.IsSubclassSchema) -> JsonSchemaValue:
924 """Handles JSON schema generation for a core schema that checks if a value is a subclass of a class.
926 For backwards compatibility with v1, this does not raise an error, but can be overridden to change this.
928 Args:
929 schema: The core schema.
931 Returns:
932 The generated JSON schema.
933 """
934 # Note: This is for compatibility with V1; you can override if you want different behavior.
935 return {}
937 def callable_schema(self, schema: core_schema.CallableSchema) -> JsonSchemaValue:
938 """Generates a JSON schema that matches a callable value.
940 Unless overridden in a subclass, this raises an error.
942 Args:
943 schema: The core schema.
945 Returns:
946 The generated JSON schema.
947 """
948 return self.handle_invalid_for_json_schema(schema, 'core_schema.CallableSchema')
950 def list_schema(self, schema: core_schema.ListSchema) -> JsonSchemaValue:
951 """Returns a schema that matches a list schema.
953 Args:
954 schema: The core schema.
956 Returns:
957 The generated JSON schema.
958 """
959 items_schema = {} if 'items_schema' not in schema else self.generate_inner(schema['items_schema'])
960 json_schema = {'type': 'array', 'items': items_schema}
961 self.update_with_validations(json_schema, schema, self.ValidationsMapping.array)
962 return json_schema
964 @deprecated('`tuple_positional_schema` is deprecated. Use `tuple_schema` instead.', category=None)
965 @final
966 def tuple_positional_schema(self, schema: core_schema.TupleSchema) -> JsonSchemaValue:
967 """Replaced by `tuple_schema`."""
968 warnings.warn(
969 '`tuple_positional_schema` is deprecated. Use `tuple_schema` instead.',
970 PydanticDeprecatedSince26,
971 stacklevel=2,
972 )
973 return self.tuple_schema(schema)
975 @deprecated('`tuple_variable_schema` is deprecated. Use `tuple_schema` instead.', category=None)
976 @final
977 def tuple_variable_schema(self, schema: core_schema.TupleSchema) -> JsonSchemaValue:
978 """Replaced by `tuple_schema`."""
979 warnings.warn(
980 '`tuple_variable_schema` is deprecated. Use `tuple_schema` instead.',
981 PydanticDeprecatedSince26,
982 stacklevel=2,
983 )
984 return self.tuple_schema(schema)
986 def tuple_schema(self, schema: core_schema.TupleSchema) -> JsonSchemaValue:
987 """Generates a JSON schema that matches a tuple schema e.g. `tuple[int,
988 str, bool]` or `tuple[int, ...]`.
990 Args:
991 schema: The core schema.
993 Returns:
994 The generated JSON schema.
995 """
996 json_schema: JsonSchemaValue = {'type': 'array'}
997 if 'variadic_item_index' in schema:
998 variadic_item_index = schema['variadic_item_index']
999 if variadic_item_index > 0:
1000 json_schema['minItems'] = variadic_item_index
1001 json_schema['prefixItems'] = [
1002 self.generate_inner(item) for item in schema['items_schema'][:variadic_item_index]
1003 ]
1004 if variadic_item_index + 1 == len(schema['items_schema']):
1005 # if the variadic item is the last item, then represent it faithfully
1006 json_schema['items'] = self.generate_inner(schema['items_schema'][variadic_item_index])
1007 else:
1008 # otherwise, 'items' represents the schema for the variadic
1009 # item plus the suffix, so just allow anything for simplicity
1010 # for now
1011 json_schema['items'] = True
1012 else:
1013 prefixItems = [self.generate_inner(item) for item in schema['items_schema']]
1014 if prefixItems:
1015 json_schema['prefixItems'] = prefixItems
1016 json_schema['minItems'] = len(prefixItems)
1017 json_schema['maxItems'] = len(prefixItems)
1018 self.update_with_validations(json_schema, schema, self.ValidationsMapping.array)
1019 return json_schema
1021 def set_schema(self, schema: core_schema.SetSchema) -> JsonSchemaValue:
1022 """Generates a JSON schema that matches a set schema.
1024 Args:
1025 schema: The core schema.
1027 Returns:
1028 The generated JSON schema.
1029 """
1030 return self._common_set_schema(schema)
1032 def frozenset_schema(self, schema: core_schema.FrozenSetSchema) -> JsonSchemaValue:
1033 """Generates a JSON schema that matches a frozenset schema.
1035 Args:
1036 schema: The core schema.
1038 Returns:
1039 The generated JSON schema.
1040 """
1041 return self._common_set_schema(schema)
1043 def _common_set_schema(self, schema: core_schema.SetSchema | core_schema.FrozenSetSchema) -> JsonSchemaValue:
1044 items_schema = {} if 'items_schema' not in schema else self.generate_inner(schema['items_schema'])
1045 json_schema = {'type': 'array', 'uniqueItems': True, 'items': items_schema}
1046 self.update_with_validations(json_schema, schema, self.ValidationsMapping.array)
1047 return json_schema
1049 def generator_schema(self, schema: core_schema.GeneratorSchema) -> JsonSchemaValue:
1050 """Returns a JSON schema that represents the provided GeneratorSchema.
1052 Args:
1053 schema: The schema.
1055 Returns:
1056 The generated JSON schema.
1057 """
1058 items_schema = {} if 'items_schema' not in schema else self.generate_inner(schema['items_schema'])
1059 json_schema = {'type': 'array', 'items': items_schema}
1060 self.update_with_validations(json_schema, schema, self.ValidationsMapping.array)
1061 return json_schema
1063 def dict_schema(self, schema: core_schema.DictSchema) -> JsonSchemaValue:
1064 """Generates a JSON schema that matches a dict schema.
1066 Args:
1067 schema: The core schema.
1069 Returns:
1070 The generated JSON schema.
1071 """
1072 json_schema: JsonSchemaValue = {'type': 'object'}
1074 keys_schema = self.generate_inner(schema['keys_schema']).copy() if 'keys_schema' in schema else {}
1075 if '$ref' not in keys_schema:
1076 keys_pattern = keys_schema.pop('pattern', None)
1077 # Don't give a title to patternProperties/propertyNames:
1078 keys_schema.pop('title', None)
1079 else:
1080 # Here, we assume that if the keys schema is a definition reference,
1081 # it can't be a simple string core schema (and thus no pattern can exist).
1082 # However, this is only in practice (in theory, a definition reference core
1083 # schema could be generated for a simple string schema).
1084 # Note that we avoid calling `self.resolve_ref_schema`, as it might not exist yet.
1085 keys_pattern = None
1087 values_schema = self.generate_inner(schema['values_schema']).copy() if 'values_schema' in schema else {}
1088 # don't give a title to additionalProperties:
1089 values_schema.pop('title', None)
1091 if values_schema or keys_pattern is not None:
1092 if keys_pattern is None:
1093 json_schema['additionalProperties'] = values_schema
1094 else:
1095 json_schema['patternProperties'] = {keys_pattern: values_schema}
1096 else: # for `dict[str, Any]`, we allow any key and any value, since `str` is the default key type
1097 json_schema['additionalProperties'] = True
1099 if (
1100 # The len check indicates that constraints are probably present:
1101 (keys_schema.get('type') == 'string' and len(keys_schema) > 1)
1102 # If this is a definition reference schema, it most likely has constraints:
1103 or '$ref' in keys_schema
1104 ):
1105 keys_schema.pop('type', None)
1106 json_schema['propertyNames'] = keys_schema
1108 self.update_with_validations(json_schema, schema, self.ValidationsMapping.object)
1109 return json_schema
1111 def function_before_schema(self, schema: core_schema.BeforeValidatorFunctionSchema) -> JsonSchemaValue:
1112 """Generates a JSON schema that matches a function-before schema.
1114 Args:
1115 schema: The core schema.
1117 Returns:
1118 The generated JSON schema.
1119 """
1120 if self.mode == 'validation' and (input_schema := schema.get('json_schema_input_schema')):
1121 return self.generate_inner(input_schema)
1123 return self.generate_inner(schema['schema'])
1125 def function_after_schema(self, schema: core_schema.AfterValidatorFunctionSchema) -> JsonSchemaValue:
1126 """Generates a JSON schema that matches a function-after schema.
1128 Args:
1129 schema: The core schema.
1131 Returns:
1132 The generated JSON schema.
1133 """
1134 return self.generate_inner(schema['schema'])
1136 def function_plain_schema(self, schema: core_schema.PlainValidatorFunctionSchema) -> JsonSchemaValue:
1137 """Generates a JSON schema that matches a function-plain schema.
1139 Args:
1140 schema: The core schema.
1142 Returns:
1143 The generated JSON schema.
1144 """
1145 if self.mode == 'validation' and (input_schema := schema.get('json_schema_input_schema')):
1146 return self.generate_inner(input_schema)
1148 return self.handle_invalid_for_json_schema(
1149 schema, f'core_schema.PlainValidatorFunctionSchema ({schema["function"]})'
1150 )
1152 def function_wrap_schema(self, schema: core_schema.WrapValidatorFunctionSchema) -> JsonSchemaValue:
1153 """Generates a JSON schema that matches a function-wrap schema.
1155 Args:
1156 schema: The core schema.
1158 Returns:
1159 The generated JSON schema.
1160 """
1161 if self.mode == 'validation' and (input_schema := schema.get('json_schema_input_schema')):
1162 return self.generate_inner(input_schema)
1164 return self.generate_inner(schema['schema'])
1166 def default_schema(self, schema: core_schema.WithDefaultSchema) -> JsonSchemaValue:
1167 """Generates a JSON schema that matches a schema with a default value.
1169 Args:
1170 schema: The core schema.
1172 Returns:
1173 The generated JSON schema.
1174 """
1175 json_schema = self.generate_inner(schema['schema'])
1177 default = self.get_default_value(schema)
1178 if default is NoDefault or default is MISSING:
1179 return json_schema
1181 # we reflect the application of custom plain, no-info serializers to defaults for
1182 # JSON Schemas viewed in serialization mode:
1183 # TODO: improvements along with https://github.com/pydantic/pydantic/issues/8208
1184 if self.mode == 'serialization':
1185 # `_get_ser_schema_for_default_value()` is used to unpack potentially nested validator schemas:
1186 ser_schema = _get_ser_schema_for_default_value(schema['schema'])
1187 if (
1188 ser_schema is not None
1189 and (ser_func := ser_schema.get('function'))
1190 and not (default is None and ser_schema.get('when_used') in ('unless-none', 'json-unless-none'))
1191 ):
1192 try:
1193 default = ser_func(default) # type: ignore
1194 except Exception:
1195 # It might be that the provided default needs to be validated (read: parsed) first
1196 # (assuming `validate_default` is enabled). However, we can't perform
1197 # such validation during JSON Schema generation so we don't support
1198 # this pattern for now.
1199 # (One example is when using `foo: ByteSize = '1MB'`, which validates and
1200 # serializes as an int. In this case, `ser_func` is `int` and `int('1MB')` fails).
1201 self.emit_warning(
1202 'non-serializable-default',
1203 f'Unable to serialize value {default!r} with the plain serializer; excluding default from JSON schema',
1204 )
1205 return json_schema
1207 try:
1208 encoded_default = self.encode_default(default)
1209 except pydantic_core.PydanticSerializationError:
1210 self.emit_warning(
1211 'non-serializable-default',
1212 f'Default value {default} is not JSON serializable; excluding default from JSON schema',
1213 )
1214 # Return the inner schema, as though there was no default
1215 return json_schema
1217 json_schema['default'] = encoded_default
1218 return json_schema
1220 def get_default_value(self, schema: core_schema.WithDefaultSchema) -> Any:
1221 """Get the default value to be used when generating a JSON Schema for a core schema with a default.
1223 The default implementation is to use the statically defined default value. This method can be overridden
1224 if you want to make use of the default factory.
1226 Args:
1227 schema: The `'with-default'` core schema.
1229 Returns:
1230 The default value to use, or [`NoDefault`][pydantic.json_schema.NoDefault] if no default
1231 value is available.
1232 """
1233 return schema.get('default', NoDefault)
1235 def nullable_schema(self, schema: core_schema.NullableSchema) -> JsonSchemaValue:
1236 """Generates a JSON schema that matches a schema that allows null values.
1238 Args:
1239 schema: The core schema.
1241 Returns:
1242 The generated JSON schema.
1243 """
1244 null_schema = {'type': 'null'}
1245 inner_json_schema = self.generate_inner(schema['schema'])
1247 if inner_json_schema == null_schema:
1248 return null_schema
1249 else:
1250 return self.get_union_of_schemas([inner_json_schema, null_schema])
1252 def union_schema(self, schema: core_schema.UnionSchema) -> JsonSchemaValue:
1253 """Generates a JSON schema that matches a schema that allows values matching any of the given schemas.
1255 Args:
1256 schema: The core schema.
1258 Returns:
1259 The generated JSON schema.
1260 """
1261 generated: list[JsonSchemaValue] = []
1263 choices = schema['choices']
1264 for choice in choices:
1265 # choice will be a tuple if an explicit label was provided
1266 choice_schema = choice[0] if isinstance(choice, tuple) else choice
1267 try:
1268 generated.append(self.generate_inner(choice_schema))
1269 except PydanticOmit:
1270 continue
1271 except PydanticInvalidForJsonSchema as exc:
1272 self.emit_warning('skipped-choice', exc.message)
1273 if len(generated) == 1:
1274 return generated[0]
1275 return self.get_union_of_schemas(generated)
1277 def get_union_of_schemas(self, schemas: list[JsonSchemaValue]) -> JsonSchemaValue:
1278 """Returns the JSON Schema representation for the union of the provided JSON Schemas.
1280 The result depends on the configured `'union_format'`.
1282 Args:
1283 schemas: The list of JSON Schemas to be included in the union.
1285 Returns:
1286 The JSON Schema representing the union of schemas.
1287 """
1288 if self.union_format == 'primitive_type_array':
1289 types: list[str] = []
1290 for schema in schemas:
1291 schema_types: list[str] | str | None = schema.get('type')
1292 if schema_types is None:
1293 # No type, meaning it can be a ref or an empty schema.
1294 break
1295 if not isinstance(schema_types, list):
1296 schema_types = [schema_types]
1297 if not all(t in _PRIMITIVE_JSON_SCHEMA_TYPES for t in schema_types):
1298 break
1299 if len(schema) != 1:
1300 # We only want to include types that don't have any constraints. For instance,
1301 # if `schemas = [{'type': 'string', 'maxLength': 3}, {'type': 'string', 'minLength': 5}]`,
1302 # we don't want to produce `{'type': 'string', 'maxLength': 3, 'minLength': 5}`.
1303 # Same if we have some metadata (e.g. `title`) on a specific union member, we want to preserve it.
1304 break
1306 types.extend(schema_types)
1307 else:
1308 # If we got there, all the schemas where valid to be used with the `'primitive_type_array` format
1309 return {'type': list(dict.fromkeys(types))}
1311 return self.get_flattened_anyof(schemas)
1313 def tagged_union_schema(self, schema: core_schema.TaggedUnionSchema) -> JsonSchemaValue:
1314 """Generates a JSON schema that matches a schema that allows values matching any of the given schemas, where
1315 the schemas are tagged with a discriminator field that indicates which schema should be used to validate
1316 the value.
1318 Args:
1319 schema: The core schema.
1321 Returns:
1322 The generated JSON schema.
1323 """
1324 generated: dict[str, JsonSchemaValue] = {}
1325 for k, v in schema['choices'].items():
1326 if isinstance(k, Enum):
1327 k = k.value
1328 try:
1329 # Use str(k) since keys must be strings for json; while not technically correct,
1330 # it's the closest that can be represented in valid JSON
1331 generated[str(k)] = self.generate_inner(v).copy()
1332 except PydanticOmit:
1333 continue
1334 except PydanticInvalidForJsonSchema as exc:
1335 self.emit_warning('skipped-choice', exc.message)
1337 one_of_choices = _deduplicate_schemas(generated.values())
1338 json_schema: JsonSchemaValue = {'oneOf': one_of_choices}
1340 # This reflects the v1 behavior; TODO: we should make it possible to exclude OpenAPI stuff from the JSON schema
1341 openapi_discriminator = self._extract_discriminator(schema, one_of_choices)
1342 if openapi_discriminator is not None:
1343 json_schema['discriminator'] = {
1344 'propertyName': openapi_discriminator,
1345 'mapping': {k: v.get('$ref', v) for k, v in generated.items()},
1346 }
1348 return json_schema
1350 def _extract_discriminator(
1351 self, schema: core_schema.TaggedUnionSchema, one_of_choices: list[JsonDict]
1352 ) -> str | None:
1353 """Extract a compatible OpenAPI discriminator from the schema and one_of choices that end up in the final
1354 schema."""
1355 openapi_discriminator: str | None = None
1357 if isinstance(schema['discriminator'], str):
1358 return schema['discriminator']
1360 if isinstance(schema['discriminator'], list):
1361 # If the discriminator is a single item list containing a string, that is equivalent to the string case
1362 if len(schema['discriminator']) == 1 and isinstance(schema['discriminator'][0], str):
1363 return schema['discriminator'][0]
1364 # When an alias is used that is different from the field name, the discriminator will be a list of single
1365 # str lists, one for the attribute and one for the actual alias. The logic here will work even if there is
1366 # more than one possible attribute, and looks for whether a single alias choice is present as a documented
1367 # property on all choices. If so, that property will be used as the OpenAPI discriminator.
1368 for alias_path in schema['discriminator']:
1369 if not isinstance(alias_path, list):
1370 break # this means that the discriminator is not a list of alias paths
1371 if len(alias_path) != 1:
1372 continue # this means that the "alias" does not represent a single field
1373 alias = alias_path[0]
1374 if not isinstance(alias, str):
1375 continue # this means that the "alias" does not represent a field
1376 alias_is_present_on_all_choices = True
1377 for choice in one_of_choices:
1378 try:
1379 choice = self.resolve_ref_schema(choice)
1380 except RuntimeError as exc:
1381 # TODO: fixme - this is a workaround for the fact that we can't always resolve refs
1382 # for tagged union choices at this point in the schema gen process, we might need to do
1383 # another pass at the end like we do for core schemas
1384 self.emit_warning('skipped-discriminator', str(exc))
1385 choice = {}
1386 properties = choice.get('properties', {})
1387 if not isinstance(properties, dict) or alias not in properties:
1388 alias_is_present_on_all_choices = False
1389 break
1390 if alias_is_present_on_all_choices:
1391 openapi_discriminator = alias
1392 break
1393 return openapi_discriminator
1395 def chain_schema(self, schema: core_schema.ChainSchema) -> JsonSchemaValue:
1396 """Generates a JSON schema that matches a core_schema.ChainSchema.
1398 When generating a schema for validation, we return the validation JSON schema for the first step in the chain.
1399 For serialization, we return the serialization JSON schema for the last step in the chain.
1401 Args:
1402 schema: The core schema.
1404 Returns:
1405 The generated JSON schema.
1406 """
1407 step_index = 0 if self.mode == 'validation' else -1 # use first step for validation, last for serialization
1408 return self.generate_inner(schema['steps'][step_index])
1410 def lax_or_strict_schema(self, schema: core_schema.LaxOrStrictSchema) -> JsonSchemaValue:
1411 """Generates a JSON schema that matches a schema that allows values matching either the lax schema or the
1412 strict schema.
1414 Args:
1415 schema: The core schema.
1417 Returns:
1418 The generated JSON schema.
1419 """
1420 # TODO: Need to read the default value off of model config or whatever
1421 use_strict = schema.get('strict', False) # TODO: replace this default False
1422 # If your JSON schema fails to generate it is probably
1423 # because one of the following two branches failed.
1424 if use_strict:
1425 return self.generate_inner(schema['strict_schema'])
1426 else:
1427 return self.generate_inner(schema['lax_schema'])
1429 def json_or_python_schema(self, schema: core_schema.JsonOrPythonSchema) -> JsonSchemaValue:
1430 """Generates a JSON schema that matches a schema that allows values matching either the JSON schema or the
1431 Python schema.
1433 The JSON schema is used instead of the Python schema. If you want to use the Python schema, you should override
1434 this method.
1436 Args:
1437 schema: The core schema.
1439 Returns:
1440 The generated JSON schema.
1441 """
1442 return self.generate_inner(schema['json_schema'])
1444 def typed_dict_schema(self, schema: core_schema.TypedDictSchema) -> JsonSchemaValue:
1445 """Generates a JSON schema that matches a schema that defines a typed dict.
1447 Args:
1448 schema: The core schema.
1450 Returns:
1451 The generated JSON schema.
1452 """
1453 total = schema.get('total', True)
1454 named_required_fields: list[tuple[str, bool, CoreSchemaField]] = [
1455 (name, self.field_is_required(field, total), field)
1456 for name, field in schema['fields'].items()
1457 if self.field_is_present(field)
1458 ]
1459 if self.mode == 'serialization':
1460 named_required_fields.extend(self._name_required_computed_fields(schema.get('computed_fields', [])))
1461 cls = schema.get('cls')
1462 config = _get_typed_dict_config(cls)
1463 with self._config_wrapper_stack.push(config):
1464 json_schema = self._named_required_fields_schema(named_required_fields)
1466 # There's some duplication between `extra_behavior` and
1467 # the config's `extra`/core config's `extra_fields_behavior`.
1468 # However, it is common to manually create TypedDictSchemas,
1469 # where you don't necessarily have a class.
1470 # At runtime, `extra_behavior` takes priority over the config
1471 # for validation, so follow the same for the JSON Schema:
1472 if schema.get('extra_behavior') == 'forbid':
1473 json_schema['additionalProperties'] = False
1474 elif schema.get('extra_behavior') == 'allow':
1475 if 'extras_schema' in schema and schema['extras_schema'] != {'type': 'any'}:
1476 json_schema['additionalProperties'] = self.generate_inner(schema['extras_schema'])
1477 else:
1478 json_schema['additionalProperties'] = True
1480 if cls is not None:
1481 # `_update_class_schema()` will not override
1482 # `additionalProperties` if already present:
1483 self._update_class_schema(json_schema, cls, config)
1484 elif 'additionalProperties' not in json_schema:
1485 extra = schema.get('config', {}).get('extra_fields_behavior')
1486 if extra == 'forbid':
1487 json_schema['additionalProperties'] = False
1488 elif extra == 'allow':
1489 json_schema['additionalProperties'] = True
1491 return json_schema
1493 @staticmethod
1494 def _name_required_computed_fields(
1495 computed_fields: list[ComputedField],
1496 ) -> list[tuple[str, bool, core_schema.ComputedField]]:
1497 return [(field['property_name'], True, field) for field in computed_fields]
1499 def _named_required_fields_schema(
1500 self, named_required_fields: Sequence[tuple[str, bool, CoreSchemaField]]
1501 ) -> JsonSchemaValue:
1502 properties: dict[str, JsonSchemaValue] = {}
1503 required_fields: list[str] = []
1504 for name, required, field in named_required_fields:
1505 if self.by_alias:
1506 name = self._get_alias_name(field, name)
1507 try:
1508 field_json_schema = self.generate_inner(field).copy()
1509 except PydanticOmit:
1510 continue
1511 if 'title' not in field_json_schema and self.field_title_should_be_set(field):
1512 title = self.get_title_from_name(name)
1513 field_json_schema['title'] = title
1514 field_json_schema = self.handle_ref_overrides(field_json_schema)
1515 properties[name] = field_json_schema
1516 if required:
1517 required_fields.append(name)
1519 json_schema = {'type': 'object', 'properties': properties}
1520 if required_fields:
1521 json_schema['required'] = required_fields
1522 return json_schema
1524 def _get_alias_name(self, field: CoreSchemaField, name: str) -> str:
1525 if field['type'] == 'computed-field':
1526 alias: Any = field.get('alias', name)
1527 elif self.mode == 'validation':
1528 alias = field.get('validation_alias', name)
1529 else:
1530 alias = field.get('serialization_alias', name)
1531 if isinstance(alias, str):
1532 name = alias
1533 elif isinstance(alias, list):
1534 alias = cast('list[str] | str', alias)
1535 for path in alias:
1536 if isinstance(path, list) and len(path) == 1 and isinstance(path[0], str):
1537 # Use the first valid single-item string path; the code that constructs the alias array
1538 # should ensure the first such item is what belongs in the JSON schema
1539 name = path[0]
1540 break
1541 else:
1542 assert_never(alias)
1543 return name
1545 def typed_dict_field_schema(self, schema: core_schema.TypedDictField) -> JsonSchemaValue:
1546 """Generates a JSON schema that matches a schema that defines a typed dict field.
1548 Args:
1549 schema: The core schema.
1551 Returns:
1552 The generated JSON schema.
1553 """
1554 return self.generate_inner(schema['schema'])
1556 def dataclass_field_schema(self, schema: core_schema.DataclassField) -> JsonSchemaValue:
1557 """Generates a JSON schema that matches a schema that defines a dataclass field.
1559 Args:
1560 schema: The core schema.
1562 Returns:
1563 The generated JSON schema.
1564 """
1565 return self.generate_inner(schema['schema'])
1567 def model_field_schema(self, schema: core_schema.ModelField) -> JsonSchemaValue:
1568 """Generates a JSON schema that matches a schema that defines a model field.
1570 Args:
1571 schema: The core schema.
1573 Returns:
1574 The generated JSON schema.
1575 """
1576 return self.generate_inner(schema['schema'])
1578 def computed_field_schema(self, schema: core_schema.ComputedField) -> JsonSchemaValue:
1579 """Generates a JSON schema that matches a schema that defines a computed field.
1581 Args:
1582 schema: The core schema.
1584 Returns:
1585 The generated JSON schema.
1586 """
1587 return self.generate_inner(schema['return_schema'])
1589 def model_schema(self, schema: core_schema.ModelSchema) -> JsonSchemaValue:
1590 """Generates a JSON schema that matches a schema that defines a model.
1592 Args:
1593 schema: The core schema.
1595 Returns:
1596 The generated JSON schema.
1597 """
1598 # We do not use schema['model'].model_json_schema() here
1599 # because it could lead to inconsistent refs handling, etc.
1600 cls = cast('type[BaseModel]', schema['cls'])
1601 config = cls.model_config
1603 with self._config_wrapper_stack.push(config):
1604 json_schema = self.generate_inner(schema['schema'])
1606 self._update_class_schema(json_schema, cls, config)
1608 return json_schema
1610 def _update_class_schema(self, json_schema: JsonSchemaValue, cls: type[Any], config: ConfigDict) -> None:
1611 """Update json_schema with the following, extracted from `config` and `cls`:
1613 * title
1614 * description
1615 * additional properties
1616 * json_schema_extra
1617 * deprecated
1619 Done in place, hence there's no return value as the original json_schema is mutated.
1620 No ref resolving is involved here, as that's not appropriate for simple updates.
1621 """
1622 from .main import BaseModel
1623 from .root_model import RootModel
1625 if (config_title := config.get('title')) is not None:
1626 json_schema.setdefault('title', config_title)
1627 elif model_title_generator := config.get('model_title_generator'):
1628 title = model_title_generator(cls)
1629 if not isinstance(title, str):
1630 raise TypeError(f'model_title_generator {model_title_generator} must return str, not {title.__class__}')
1631 json_schema.setdefault('title', title)
1632 if 'title' not in json_schema:
1633 json_schema['title'] = cls.__name__
1635 # BaseModel and dataclasses; don't use cls.__doc__ as it will contain the verbose class signature by default
1636 docstring = None if cls is BaseModel or dataclasses.is_dataclass(cls) else cls.__doc__
1638 if docstring:
1639 json_schema.setdefault('description', inspect.cleandoc(docstring))
1640 elif issubclass(cls, RootModel) and (root_description := cls.__pydantic_fields__['root'].description):
1641 json_schema.setdefault('description', root_description)
1643 extra = config.get('extra')
1644 if 'additionalProperties' not in json_schema: # This check is particularly important for `typed_dict_schema()`
1645 if extra == 'allow':
1646 json_schema['additionalProperties'] = True
1647 elif extra == 'forbid':
1648 json_schema['additionalProperties'] = False
1650 json_schema_extra = config.get('json_schema_extra')
1651 if issubclass(cls, BaseModel) and cls.__pydantic_root_model__:
1652 root_json_schema_extra = cls.model_fields['root'].json_schema_extra
1653 if json_schema_extra and root_json_schema_extra:
1654 raise ValueError(
1655 '"model_config[\'json_schema_extra\']" and "Field.json_schema_extra" on "RootModel.root"'
1656 ' field must not be set simultaneously'
1657 )
1658 if root_json_schema_extra:
1659 json_schema_extra = root_json_schema_extra
1661 if isinstance(json_schema_extra, (staticmethod, classmethod)):
1662 # In older versions of python, this is necessary to ensure staticmethod/classmethods are callable
1663 json_schema_extra = json_schema_extra.__get__(cls)
1665 if isinstance(json_schema_extra, dict):
1666 json_schema.update(json_schema_extra)
1667 elif callable(json_schema_extra):
1668 # FIXME: why are there type ignores here? We support two signatures for json_schema_extra callables...
1669 if len(inspect.signature(json_schema_extra).parameters) > 1:
1670 json_schema_extra(json_schema, cls) # type: ignore
1671 else:
1672 json_schema_extra(json_schema) # type: ignore
1673 elif json_schema_extra is not None:
1674 raise ValueError(
1675 f"model_config['json_schema_extra']={json_schema_extra} should be a dict, callable, or None"
1676 )
1678 if hasattr(cls, '__deprecated__'):
1679 json_schema['deprecated'] = True
1681 def resolve_ref_schema(self, json_schema: JsonSchemaValue) -> JsonSchemaValue:
1682 """Resolve a JsonSchemaValue to the non-ref schema if it is a $ref schema.
1684 Args:
1685 json_schema: The schema to resolve.
1687 Returns:
1688 The resolved schema.
1690 Raises:
1691 RuntimeError: If the schema reference can't be found in definitions.
1692 """
1693 while '$ref' in json_schema:
1694 ref = json_schema['$ref']
1695 schema_to_update = self.get_schema_from_definitions(JsonRef(ref))
1696 if schema_to_update is None:
1697 raise RuntimeError(f'Cannot update undefined schema for $ref={ref}')
1698 json_schema = schema_to_update
1699 return json_schema
1701 def model_fields_schema(self, schema: core_schema.ModelFieldsSchema) -> JsonSchemaValue:
1702 """Generates a JSON schema that matches a schema that defines a model's fields.
1704 Args:
1705 schema: The core schema.
1707 Returns:
1708 The generated JSON schema.
1709 """
1710 named_required_fields: list[tuple[str, bool, CoreSchemaField]] = [
1711 (name, self.field_is_required(field, total=True), field)
1712 for name, field in schema['fields'].items()
1713 if self.field_is_present(field)
1714 ]
1715 if self.mode == 'serialization':
1716 named_required_fields.extend(self._name_required_computed_fields(schema.get('computed_fields', [])))
1717 json_schema = self._named_required_fields_schema(named_required_fields)
1718 extras_schema = schema.get('extras_schema', None)
1719 if extras_schema is not None:
1720 schema_to_update = self.resolve_ref_schema(json_schema)
1721 schema_to_update['additionalProperties'] = self.generate_inner(extras_schema)
1722 return json_schema
1724 def field_is_present(self, field: CoreSchemaField) -> bool:
1725 """Whether the field should be included in the generated JSON schema.
1727 Args:
1728 field: The schema for the field itself.
1730 Returns:
1731 `True` if the field should be included in the generated JSON schema, `False` otherwise.
1732 """
1733 if self.mode == 'serialization':
1734 # If you still want to include the field in the generated JSON schema,
1735 # override this method and return True
1736 return not field.get('serialization_exclude')
1737 elif self.mode == 'validation':
1738 return True
1739 else:
1740 assert_never(self.mode)
1742 def field_is_required(
1743 self,
1744 field: core_schema.ModelField | core_schema.DataclassField | core_schema.TypedDictField,
1745 total: bool,
1746 ) -> bool:
1747 """Whether the field should be marked as required in the generated JSON schema.
1748 (Note that this is irrelevant if the field is not present in the JSON schema.).
1750 Args:
1751 field: The schema for the field itself.
1752 total: Only applies to `TypedDictField`s.
1753 Indicates if the `TypedDict` this field belongs to is total, in which case any fields that don't
1754 explicitly specify `required=False` are required.
1756 Returns:
1757 `True` if the field should be marked as required in the generated JSON schema, `False` otherwise.
1758 """
1759 if self.mode == 'serialization' and self._config.json_schema_serialization_defaults_required:
1760 return not field.get('serialization_exclude')
1761 else:
1762 if field['type'] == 'typed-dict-field':
1763 return field.get('required', total)
1764 else:
1765 return field['schema']['type'] != 'default'
1767 def dataclass_args_schema(self, schema: core_schema.DataclassArgsSchema) -> JsonSchemaValue:
1768 """Generates a JSON schema that matches a schema that defines a dataclass's constructor arguments.
1770 Args:
1771 schema: The core schema.
1773 Returns:
1774 The generated JSON schema.
1775 """
1776 named_required_fields: list[tuple[str, bool, CoreSchemaField]] = [
1777 (field['name'], self.field_is_required(field, total=True), field)
1778 for field in schema['fields']
1779 if self.field_is_present(field)
1780 ]
1781 if self.mode == 'serialization':
1782 named_required_fields.extend(self._name_required_computed_fields(schema.get('computed_fields', [])))
1783 return self._named_required_fields_schema(named_required_fields)
1785 def dataclass_schema(self, schema: core_schema.DataclassSchema) -> JsonSchemaValue:
1786 """Generates a JSON schema that matches a schema that defines a dataclass.
1788 Args:
1789 schema: The core schema.
1791 Returns:
1792 The generated JSON schema.
1793 """
1794 from ._internal._dataclasses import is_stdlib_dataclass
1796 cls = schema['cls']
1797 config: ConfigDict = getattr(cls, '__pydantic_config__', cast('ConfigDict', {}))
1799 with self._config_wrapper_stack.push(config):
1800 json_schema = self.generate_inner(schema['schema']).copy()
1802 self._update_class_schema(json_schema, cls, config)
1804 # Dataclass-specific handling of description
1805 if is_stdlib_dataclass(cls):
1806 # vanilla dataclass; don't use cls.__doc__ as it will contain the class signature by default
1807 description = None
1808 else:
1809 description = None if cls.__doc__ is None else inspect.cleandoc(cls.__doc__)
1810 if description:
1811 json_schema['description'] = description
1813 return json_schema
1815 def arguments_schema(self, schema: core_schema.ArgumentsSchema) -> JsonSchemaValue:
1816 """Generates a JSON schema that matches a schema that defines a function's arguments.
1818 Args:
1819 schema: The core schema.
1821 Returns:
1822 The generated JSON schema.
1823 """
1824 prefer_positional = schema.get('metadata', {}).get('pydantic_js_prefer_positional_arguments')
1826 arguments = schema['arguments_schema']
1827 kw_only_arguments = [a for a in arguments if a.get('mode') == 'keyword_only']
1828 kw_or_p_arguments = [a for a in arguments if a.get('mode') in {'positional_or_keyword', None}]
1829 p_only_arguments = [a for a in arguments if a.get('mode') == 'positional_only']
1830 var_args_schema = schema.get('var_args_schema')
1831 var_kwargs_schema = schema.get('var_kwargs_schema')
1833 if prefer_positional:
1834 positional_possible = not kw_only_arguments and not var_kwargs_schema
1835 if positional_possible:
1836 return self.p_arguments_schema(p_only_arguments + kw_or_p_arguments, var_args_schema)
1838 keyword_possible = not p_only_arguments and not var_args_schema
1839 if keyword_possible:
1840 return self.kw_arguments_schema(kw_or_p_arguments + kw_only_arguments, var_kwargs_schema)
1842 if not prefer_positional:
1843 positional_possible = not kw_only_arguments and not var_kwargs_schema
1844 if positional_possible:
1845 return self.p_arguments_schema(p_only_arguments + kw_or_p_arguments, var_args_schema)
1847 raise PydanticInvalidForJsonSchema(
1848 'Unable to generate JSON schema for arguments validator with positional-only and keyword-only arguments'
1849 )
1851 def kw_arguments_schema(
1852 self, arguments: list[core_schema.ArgumentsParameter], var_kwargs_schema: CoreSchema | None
1853 ) -> JsonSchemaValue:
1854 """Generates a JSON schema that matches a schema that defines a function's keyword arguments.
1856 Args:
1857 arguments: The core schema.
1859 Returns:
1860 The generated JSON schema.
1861 """
1862 properties: dict[str, JsonSchemaValue] = {}
1863 required: list[str] = []
1864 for argument in arguments:
1865 name = self.get_argument_name(argument)
1866 argument_schema = self.generate_inner(argument['schema']).copy()
1867 if 'title' not in argument_schema and self.field_title_should_be_set(argument['schema']):
1868 argument_schema['title'] = self.get_title_from_name(name)
1869 properties[name] = argument_schema
1871 if argument['schema']['type'] != 'default':
1872 # This assumes that if the argument has a default value,
1873 # the inner schema must be of type WithDefaultSchema.
1874 # I believe this is true, but I am not 100% sure
1875 required.append(name)
1877 json_schema: JsonSchemaValue = {'type': 'object', 'properties': properties}
1878 if required:
1879 json_schema['required'] = required
1881 if var_kwargs_schema:
1882 additional_properties_schema = self.generate_inner(var_kwargs_schema)
1883 if additional_properties_schema:
1884 json_schema['additionalProperties'] = additional_properties_schema
1885 else:
1886 json_schema['additionalProperties'] = False
1887 return json_schema
1889 def p_arguments_schema(
1890 self, arguments: list[core_schema.ArgumentsParameter], var_args_schema: CoreSchema | None
1891 ) -> JsonSchemaValue:
1892 """Generates a JSON schema that matches a schema that defines a function's positional arguments.
1894 Args:
1895 arguments: The core schema.
1897 Returns:
1898 The generated JSON schema.
1899 """
1900 prefix_items: list[JsonSchemaValue] = []
1901 min_items = 0
1903 for argument in arguments:
1904 name = self.get_argument_name(argument)
1906 argument_schema = self.generate_inner(argument['schema']).copy()
1907 if 'title' not in argument_schema and self.field_title_should_be_set(argument['schema']):
1908 argument_schema['title'] = self.get_title_from_name(name)
1909 prefix_items.append(argument_schema)
1911 if argument['schema']['type'] != 'default':
1912 # This assumes that if the argument has a default value,
1913 # the inner schema must be of type WithDefaultSchema.
1914 # I believe this is true, but I am not 100% sure
1915 min_items += 1
1917 json_schema: JsonSchemaValue = {'type': 'array'}
1918 if prefix_items:
1919 json_schema['prefixItems'] = prefix_items
1920 if min_items:
1921 json_schema['minItems'] = min_items
1923 if var_args_schema:
1924 items_schema = self.generate_inner(var_args_schema)
1925 if items_schema:
1926 json_schema['items'] = items_schema
1927 else:
1928 json_schema['maxItems'] = len(prefix_items)
1930 return json_schema
1932 def get_argument_name(self, argument: core_schema.ArgumentsParameter | core_schema.ArgumentsV3Parameter) -> str:
1933 """Retrieves the name of an argument.
1935 Args:
1936 argument: The core schema.
1938 Returns:
1939 The name of the argument.
1940 """
1941 name = argument['name']
1942 if self.by_alias:
1943 alias = argument.get('alias')
1944 if isinstance(alias, str):
1945 name = alias
1946 else:
1947 pass # might want to do something else?
1948 return name
1950 def arguments_v3_schema(self, schema: core_schema.ArgumentsV3Schema) -> JsonSchemaValue:
1951 """Generates a JSON schema that matches a schema that defines a function's arguments.
1953 Args:
1954 schema: The core schema.
1956 Returns:
1957 The generated JSON schema.
1958 """
1959 arguments = schema['arguments_schema']
1960 properties: dict[str, JsonSchemaValue] = {}
1961 required: list[str] = []
1962 for argument in arguments:
1963 mode = argument.get('mode', 'positional_or_keyword')
1964 name = self.get_argument_name(argument)
1965 argument_schema = self.generate_inner(argument['schema']).copy()
1966 if mode == 'var_args':
1967 argument_schema = {'type': 'array', 'items': argument_schema}
1968 elif mode == 'var_kwargs_uniform':
1969 argument_schema = {'type': 'object', 'additionalProperties': argument_schema}
1971 argument_schema.setdefault('title', self.get_title_from_name(name))
1972 properties[name] = argument_schema
1974 if (
1975 (mode == 'var_kwargs_unpacked_typed_dict' and 'required' in argument_schema)
1976 or mode not in {'var_args', 'var_kwargs_uniform', 'var_kwargs_unpacked_typed_dict'}
1977 and argument['schema']['type'] != 'default'
1978 ):
1979 # This assumes that if the argument has a default value,
1980 # the inner schema must be of type WithDefaultSchema.
1981 # I believe this is true, but I am not 100% sure
1982 required.append(name)
1984 json_schema: JsonSchemaValue = {'type': 'object', 'properties': properties}
1985 if required:
1986 json_schema['required'] = required
1987 return json_schema
1989 def call_schema(self, schema: core_schema.CallSchema) -> JsonSchemaValue:
1990 """Generates a JSON schema that matches a schema that defines a function call.
1992 Args:
1993 schema: The core schema.
1995 Returns:
1996 The generated JSON schema.
1997 """
1998 return self.generate_inner(schema['arguments_schema'])
2000 def custom_error_schema(self, schema: core_schema.CustomErrorSchema) -> JsonSchemaValue:
2001 """Generates a JSON schema that matches a schema that defines a custom error.
2003 Args:
2004 schema: The core schema.
2006 Returns:
2007 The generated JSON schema.
2008 """
2009 return self.generate_inner(schema['schema'])
2011 def json_schema(self, schema: core_schema.JsonSchema) -> JsonSchemaValue:
2012 """Generates a JSON schema that matches a schema that defines a JSON object.
2014 Args:
2015 schema: The core schema.
2017 Returns:
2018 The generated JSON schema.
2019 """
2020 content_core_schema = schema.get('schema') or core_schema.any_schema()
2021 content_json_schema = self.generate_inner(content_core_schema)
2022 if self.mode == 'validation':
2023 return {'type': 'string', 'contentMediaType': 'application/json', 'contentSchema': content_json_schema}
2024 else:
2025 # self.mode == 'serialization'
2026 return content_json_schema
2028 def url_schema(self, schema: core_schema.UrlSchema) -> JsonSchemaValue:
2029 """Generates a JSON schema that matches a schema that defines a URL.
2031 Args:
2032 schema: The core schema.
2034 Returns:
2035 The generated JSON schema.
2036 """
2037 json_schema = {'type': 'string', 'format': 'uri', 'minLength': 1}
2038 self.update_with_validations(json_schema, schema, self.ValidationsMapping.string)
2039 return json_schema
2041 def multi_host_url_schema(self, schema: core_schema.MultiHostUrlSchema) -> JsonSchemaValue:
2042 """Generates a JSON schema that matches a schema that defines a URL that can be used with multiple hosts.
2044 Args:
2045 schema: The core schema.
2047 Returns:
2048 The generated JSON schema.
2049 """
2050 # Note: 'multi-host-uri' is a custom/pydantic-specific format, not part of the JSON Schema spec
2051 json_schema = {'type': 'string', 'format': 'multi-host-uri', 'minLength': 1}
2052 self.update_with_validations(json_schema, schema, self.ValidationsMapping.string)
2053 return json_schema
2055 def uuid_schema(self, schema: core_schema.UuidSchema) -> JsonSchemaValue:
2056 """Generates a JSON schema that matches a UUID.
2058 Args:
2059 schema: The core schema.
2061 Returns:
2062 The generated JSON schema.
2063 """
2064 return {'type': 'string', 'format': 'uuid'}
2066 def definitions_schema(self, schema: core_schema.DefinitionsSchema) -> JsonSchemaValue:
2067 """Generates a JSON schema that matches a schema that defines a JSON object with definitions.
2069 Args:
2070 schema: The core schema.
2072 Returns:
2073 The generated JSON schema.
2074 """
2075 for definition in schema['definitions']:
2076 try:
2077 self.generate_inner(definition)
2078 except PydanticInvalidForJsonSchema as e: # noqa: PERF203
2079 core_ref: CoreRef = CoreRef(definition['ref']) # type: ignore
2080 self._core_defs_invalid_for_json_schema[self.get_defs_ref((core_ref, self.mode))] = e
2081 continue
2082 return self.generate_inner(schema['schema'])
2084 def definition_ref_schema(self, schema: core_schema.DefinitionReferenceSchema) -> JsonSchemaValue:
2085 """Generates a JSON schema that matches a schema that references a definition.
2087 Args:
2088 schema: The core schema.
2090 Returns:
2091 The generated JSON schema.
2092 """
2093 core_ref = CoreRef(schema['schema_ref'])
2094 _, ref_json_schema = self.get_cache_defs_ref_schema(core_ref)
2095 return ref_json_schema
2097 def ser_schema(
2098 self, schema: core_schema.SerSchema | core_schema.IncExSeqSerSchema | core_schema.IncExDictSerSchema
2099 ) -> JsonSchemaValue | None:
2100 """Generates a JSON schema that matches a schema that defines a serialized object.
2102 Args:
2103 schema: The core schema.
2105 Returns:
2106 The generated JSON schema.
2107 """
2108 schema_type = schema['type']
2109 if schema_type == 'function-plain' or schema_type == 'function-wrap':
2110 # PlainSerializerFunctionSerSchema or WrapSerializerFunctionSerSchema
2111 return_schema = schema.get('return_schema')
2112 if return_schema is not None:
2113 return self.generate_inner(return_schema)
2114 elif schema_type == 'format' or schema_type == 'to-string':
2115 # FormatSerSchema or ToStringSerSchema
2116 return self.str_schema(core_schema.str_schema())
2117 elif schema['type'] == 'model':
2118 # ModelSerSchema
2119 return self.generate_inner(schema['schema'])
2120 return None
2122 def complex_schema(self, schema: core_schema.ComplexSchema) -> JsonSchemaValue:
2123 """Generates a JSON schema that matches a complex number.
2125 JSON has no standard way to represent complex numbers. Complex number is not a numeric
2126 type. Here we represent complex number as strings following the rule defined by Python.
2127 For instance, '1+2j' is an accepted complex string. Details can be found in
2128 [Python's `complex` documentation][complex].
2130 Args:
2131 schema: The core schema.
2133 Returns:
2134 The generated JSON schema.
2135 """
2136 return {'type': 'string'}
2138 # ### Utility methods
2140 def get_title_from_name(self, name: str) -> str:
2141 """Retrieves a title from a name.
2143 Args:
2144 name: The name to retrieve a title from.
2146 Returns:
2147 The title.
2148 """
2149 return name.title().replace('_', ' ').strip()
2151 def field_title_should_be_set(self, schema: CoreSchemaOrField) -> bool:
2152 """Returns true if a field with the given schema should have a title set based on the field name.
2154 Intuitively, we want this to return true for schemas that wouldn't otherwise provide their own title
2155 (e.g., int, float, str), and false for those that would (e.g., BaseModel subclasses).
2157 Args:
2158 schema: The schema to check.
2160 Returns:
2161 `True` if the field should have a title set, `False` otherwise.
2162 """
2163 if _core_utils.is_core_schema_field(schema):
2164 if schema['type'] == 'computed-field':
2165 field_schema = schema['return_schema']
2166 else:
2167 field_schema = schema['schema']
2168 return self.field_title_should_be_set(field_schema)
2170 elif _core_utils.is_core_schema(schema):
2171 if schema.get('ref'): # things with refs, such as models and enums, should not have titles set
2172 return False
2173 if schema['type'] in {'default', 'nullable', 'definitions'}:
2174 return self.field_title_should_be_set(schema['schema']) # type: ignore[typeddict-item]
2175 if _core_utils.is_function_with_inner_schema(schema):
2176 return self.field_title_should_be_set(schema['schema'])
2177 if schema['type'] == 'definition-ref':
2178 # Referenced schemas should not have titles set for the same reason
2179 # schemas with refs should not
2180 return False
2181 return True # anything else should have title set
2183 else:
2184 raise PydanticInvalidForJsonSchema(f'Unexpected schema type: schema={schema}') # pragma: no cover
2186 def normalize_name(self, name: str) -> str:
2187 """Normalizes a name to be used as a key in a dictionary.
2189 Args:
2190 name: The name to normalize.
2192 Returns:
2193 The normalized name.
2194 """
2195 return re.sub(r'[^a-zA-Z0-9.\-_]', '_', name).replace('.', '__')
2197 def get_defs_ref(self, core_mode_ref: CoreModeRef) -> DefsRef:
2198 """Override this method to change the way that definitions keys are generated from a core reference.
2200 Args:
2201 core_mode_ref: The core reference.
2203 Returns:
2204 The definitions key.
2205 """
2206 # Split the core ref into "components"; generic origins and arguments are each separate components
2207 core_ref, mode = core_mode_ref
2208 components = re.split(r'([\][,])', core_ref)
2209 # Remove IDs from each component
2210 components = [x.rsplit(':', 1)[0] for x in components]
2211 core_ref_no_id = ''.join(components)
2212 # Remove everything before the last period from each "component"
2213 components = [re.sub(r'(?:[^.[\]]+\.)+((?:[^.[\]]+))', r'\1', x) for x in components]
2214 short_ref = ''.join(components)
2216 mode_title = _MODE_TITLE_MAPPING[mode]
2218 # It is important that the generated defs_ref values be such that at least one choice will not
2219 # be generated for any other core_ref. Currently, this should be the case because we include
2220 # the id of the source type in the core_ref
2221 name = DefsRef(self.normalize_name(short_ref))
2222 name_mode = DefsRef(self.normalize_name(short_ref) + f'-{mode_title}')
2223 module_qualname = DefsRef(self.normalize_name(core_ref_no_id))
2224 module_qualname_mode = DefsRef(f'{module_qualname}-{mode_title}')
2225 module_qualname_id = DefsRef(self.normalize_name(core_ref))
2226 occurrence_index = self._collision_index.get(module_qualname_id)
2227 if occurrence_index is None:
2228 self._collision_counter[module_qualname] += 1
2229 occurrence_index = self._collision_index[module_qualname_id] = self._collision_counter[module_qualname]
2231 module_qualname_occurrence = DefsRef(f'{module_qualname}__{occurrence_index}')
2232 module_qualname_occurrence_mode = DefsRef(f'{module_qualname_mode}__{occurrence_index}')
2234 self._prioritized_defsref_choices[module_qualname_occurrence_mode] = [
2235 name,
2236 name_mode,
2237 module_qualname,
2238 module_qualname_mode,
2239 module_qualname_occurrence,
2240 module_qualname_occurrence_mode,
2241 ]
2243 return module_qualname_occurrence_mode
2245 def get_cache_defs_ref_schema(self, core_ref: CoreRef) -> tuple[DefsRef, JsonSchemaValue]:
2246 """This method wraps the get_defs_ref method with some cache-lookup/population logic,
2247 and returns both the produced defs_ref and the JSON schema that will refer to the right definition.
2249 Args:
2250 core_ref: The core reference to get the definitions reference for.
2252 Returns:
2253 A tuple of the definitions reference and the JSON schema that will refer to it.
2254 """
2255 core_mode_ref = (core_ref, self.mode)
2256 maybe_defs_ref = self.core_to_defs_refs.get(core_mode_ref)
2257 if maybe_defs_ref is not None:
2258 json_ref = self.core_to_json_refs[core_mode_ref]
2259 return maybe_defs_ref, {'$ref': json_ref}
2261 defs_ref = self.get_defs_ref(core_mode_ref)
2263 # populate the ref translation mappings
2264 self.core_to_defs_refs[core_mode_ref] = defs_ref
2265 self.defs_to_core_refs[defs_ref] = core_mode_ref
2267 json_ref = JsonRef(self.ref_template.format(model=defs_ref))
2268 self.core_to_json_refs[core_mode_ref] = json_ref
2269 self.json_to_defs_refs[json_ref] = defs_ref
2270 ref_json_schema = {'$ref': json_ref}
2271 return defs_ref, ref_json_schema
2273 def handle_ref_overrides(self, json_schema: JsonSchemaValue) -> JsonSchemaValue:
2274 """Remove any sibling keys that are redundant with the referenced schema.
2276 Args:
2277 json_schema: The schema to remove redundant sibling keys from.
2279 Returns:
2280 The schema with redundant sibling keys removed.
2281 """
2282 if '$ref' in json_schema:
2283 # prevent modifications to the input; this copy may be safe to drop if there is significant overhead
2284 json_schema = json_schema.copy()
2286 referenced_json_schema = self.get_schema_from_definitions(JsonRef(json_schema['$ref']))
2287 if referenced_json_schema is None:
2288 # This can happen when building schemas for models with not-yet-defined references.
2289 # It may be a good idea to do a recursive pass at the end of the generation to remove
2290 # any redundant override keys.
2291 return json_schema
2292 for k, v in list(json_schema.items()):
2293 if k == '$ref':
2294 continue
2295 if k in referenced_json_schema and referenced_json_schema[k] == v:
2296 del json_schema[k] # redundant key
2298 return json_schema
2300 def get_schema_from_definitions(self, json_ref: JsonRef) -> JsonSchemaValue | None:
2301 try:
2302 def_ref = self.json_to_defs_refs[json_ref]
2303 if def_ref in self._core_defs_invalid_for_json_schema:
2304 raise self._core_defs_invalid_for_json_schema[def_ref]
2305 return self.definitions.get(def_ref, None)
2306 except KeyError:
2307 if json_ref.startswith(('http://', 'https://')):
2308 return None
2309 raise
2311 def encode_default(self, dft: Any) -> Any:
2312 """Encode a default value to a JSON-serializable value.
2314 This is used to encode default values for fields in the generated JSON schema.
2316 Args:
2317 dft: The default value to encode.
2319 Returns:
2320 The encoded default value.
2321 """
2322 from .type_adapter import TypeAdapter, _type_has_config
2324 config = self._config
2325 try:
2326 default = (
2327 dft
2328 if _type_has_config(type(dft))
2329 else TypeAdapter(type(dft), config=config.config_dict).dump_python(
2330 dft, by_alias=self.by_alias, mode='json'
2331 )
2332 )
2333 except PydanticSchemaGenerationError:
2334 raise pydantic_core.PydanticSerializationError(f'Unable to encode default value {dft}')
2336 return pydantic_core.to_jsonable_python(
2337 default, timedelta_mode=config.ser_json_timedelta, bytes_mode=config.ser_json_bytes, by_alias=self.by_alias
2338 )
2340 def update_with_validations(
2341 self, json_schema: JsonSchemaValue, core_schema: CoreSchema, mapping: dict[str, str]
2342 ) -> None:
2343 """Update the json_schema with the corresponding validations specified in the core_schema,
2344 using the provided mapping to translate keys in core_schema to the appropriate keys for a JSON schema.
2346 Args:
2347 json_schema: The JSON schema to update.
2348 core_schema: The core schema to get the validations from.
2349 mapping: A mapping from core_schema attribute names to the corresponding JSON schema attribute names.
2350 """
2351 for core_key, json_schema_key in mapping.items():
2352 if core_key in core_schema:
2353 json_schema[json_schema_key] = core_schema[core_key]
2355 class ValidationsMapping:
2356 """This class just contains mappings from core_schema attribute names to the corresponding
2357 JSON schema attribute names. While I suspect it is unlikely to be necessary, you can in
2358 principle override this class in a subclass of GenerateJsonSchema (by inheriting from
2359 GenerateJsonSchema.ValidationsMapping) to change these mappings.
2360 """
2362 numeric = {
2363 'multiple_of': 'multipleOf',
2364 'le': 'maximum',
2365 'ge': 'minimum',
2366 'lt': 'exclusiveMaximum',
2367 'gt': 'exclusiveMinimum',
2368 }
2369 bytes = {
2370 'min_length': 'minLength',
2371 'max_length': 'maxLength',
2372 }
2373 string = {
2374 'min_length': 'minLength',
2375 'max_length': 'maxLength',
2376 'pattern': 'pattern',
2377 }
2378 array = {
2379 'min_length': 'minItems',
2380 'max_length': 'maxItems',
2381 }
2382 object = {
2383 'min_length': 'minProperties',
2384 'max_length': 'maxProperties',
2385 }
2387 def get_flattened_anyof(self, schemas: list[JsonSchemaValue]) -> JsonSchemaValue:
2388 members = []
2389 for schema in schemas:
2390 if len(schema) == 1 and 'anyOf' in schema:
2391 members.extend(schema['anyOf'])
2392 else:
2393 members.append(schema)
2394 members = _deduplicate_schemas(members)
2395 if len(members) == 1:
2396 return members[0]
2397 return {'anyOf': members}
2399 def get_json_ref_counts(self, json_schema: JsonSchemaValue) -> dict[JsonRef, int]:
2400 """Get all values corresponding to the key '$ref' anywhere in the json_schema."""
2401 json_refs: dict[JsonRef, int] = Counter()
2403 def _add_json_refs(schema: Any) -> None:
2404 if isinstance(schema, dict):
2405 if '$ref' in schema:
2406 json_ref = JsonRef(schema['$ref'])
2407 if not isinstance(json_ref, str):
2408 return # in this case, '$ref' might have been the name of a property
2409 already_visited = json_ref in json_refs
2410 json_refs[json_ref] += 1
2411 if already_visited:
2412 return # prevent recursion on a definition that was already visited
2413 try:
2414 defs_ref = self.json_to_defs_refs[json_ref]
2415 if defs_ref in self._core_defs_invalid_for_json_schema:
2416 raise self._core_defs_invalid_for_json_schema[defs_ref]
2417 _add_json_refs(self.definitions[defs_ref])
2418 except KeyError:
2419 if not json_ref.startswith(('http://', 'https://')):
2420 raise
2422 for k, v in schema.items():
2423 if k == 'examples' and isinstance(v, list):
2424 # Skip examples that may contain arbitrary values and references
2425 # (see the comment in `_get_all_json_refs` for more details).
2426 continue
2427 _add_json_refs(v)
2428 elif isinstance(schema, list):
2429 for v in schema:
2430 _add_json_refs(v)
2432 _add_json_refs(json_schema)
2433 return json_refs
2435 def handle_invalid_for_json_schema(self, schema: CoreSchemaOrField, error_info: str) -> JsonSchemaValue:
2436 raise PydanticInvalidForJsonSchema(f'Cannot generate a JsonSchema for {error_info}')
2438 def emit_warning(self, kind: JsonSchemaWarningKind, detail: str) -> None:
2439 """This method simply emits PydanticJsonSchemaWarnings based on handling in the `warning_message` method."""
2440 message = self.render_warning_message(kind, detail)
2441 if message is not None:
2442 warnings.warn(message, PydanticJsonSchemaWarning)
2444 def render_warning_message(self, kind: JsonSchemaWarningKind, detail: str) -> str | None:
2445 """This method is responsible for ignoring warnings as desired, and for formatting the warning messages.
2447 You can override the value of `ignored_warning_kinds` in a subclass of GenerateJsonSchema
2448 to modify what warnings are generated. If you want more control, you can override this method;
2449 just return None in situations where you don't want warnings to be emitted.
2451 Args:
2452 kind: The kind of warning to render. It can be one of the following:
2454 - 'skipped-choice': A choice field was skipped because it had no valid choices.
2455 - 'non-serializable-default': A default value was skipped because it was not JSON-serializable.
2456 detail: A string with additional details about the warning.
2458 Returns:
2459 The formatted warning message, or `None` if no warning should be emitted.
2460 """
2461 if kind in self.ignored_warning_kinds:
2462 return None
2463 return f'{detail} [{kind}]'
2465 def _build_definitions_remapping(self) -> _DefinitionsRemapping:
2466 defs_to_json: dict[DefsRef, JsonRef] = {}
2467 for defs_refs in self._prioritized_defsref_choices.values():
2468 for defs_ref in defs_refs:
2469 json_ref = JsonRef(self.ref_template.format(model=defs_ref))
2470 defs_to_json[defs_ref] = json_ref
2472 return _DefinitionsRemapping.from_prioritized_choices(
2473 self._prioritized_defsref_choices, defs_to_json, self.definitions
2474 )
2476 def _garbage_collect_definitions(self, schema: JsonSchemaValue) -> None:
2477 visited_defs_refs: set[DefsRef] = set()
2478 unvisited_json_refs = _get_all_json_refs(schema)
2479 while unvisited_json_refs:
2480 next_json_ref = unvisited_json_refs.pop()
2481 try:
2482 next_defs_ref = self.json_to_defs_refs[next_json_ref]
2483 if next_defs_ref in visited_defs_refs:
2484 continue
2485 visited_defs_refs.add(next_defs_ref)
2486 unvisited_json_refs.update(_get_all_json_refs(self.definitions[next_defs_ref]))
2487 except KeyError:
2488 if not next_json_ref.startswith(('http://', 'https://')):
2489 raise
2491 self.definitions = {k: v for k, v in self.definitions.items() if k in visited_defs_refs}
2494# ##### Start JSON Schema Generation Functions #####
2497def model_json_schema(
2498 cls: type[BaseModel] | type[PydanticDataclass],
2499 by_alias: bool = True,
2500 ref_template: str = DEFAULT_REF_TEMPLATE,
2501 union_format: Literal['any_of', 'primitive_type_array'] = 'any_of',
2502 schema_generator: type[GenerateJsonSchema] = GenerateJsonSchema,
2503 mode: JsonSchemaMode = 'validation',
2504) -> dict[str, Any]:
2505 """Utility function to generate a JSON Schema for a model.
2507 Args:
2508 cls: The model class to generate a JSON Schema for.
2509 by_alias: If `True` (the default), fields will be serialized according to their alias.
2510 If `False`, fields will be serialized according to their attribute name.
2511 ref_template: The template to use for generating JSON Schema references.
2512 union_format: The format to use when combining schemas from unions together. Can be one of:
2514 - `'any_of'`: Use the [`anyOf`](https://json-schema.org/understanding-json-schema/reference/combining#anyOf)
2515 keyword to combine schemas (the default).
2516 - `'primitive_type_array'`: Use the [`type`](https://json-schema.org/understanding-json-schema/reference/type)
2517 keyword as an array of strings, containing each type of the combination. If any of the schemas is not a primitive
2518 type (`string`, `boolean`, `null`, `integer` or `number`) or contains constraints/metadata, falls back to
2519 `any_of`.
2520 schema_generator: The class to use for generating the JSON Schema.
2521 mode: The mode to use for generating the JSON Schema. It can be one of the following:
2523 - 'validation': Generate a JSON Schema for validating data.
2524 - 'serialization': Generate a JSON Schema for serializing data.
2526 Returns:
2527 The generated JSON Schema.
2528 """
2529 from .main import BaseModel
2531 schema_generator_instance = schema_generator(
2532 by_alias=by_alias, ref_template=ref_template, union_format=union_format
2533 )
2535 if isinstance(cls.__pydantic_core_schema__, _mock_val_ser.MockCoreSchema):
2536 cls.__pydantic_core_schema__.rebuild()
2538 if cls is BaseModel:
2539 raise AttributeError('model_json_schema() must be called on a subclass of BaseModel, not BaseModel itself.')
2541 assert not isinstance(cls.__pydantic_core_schema__, _mock_val_ser.MockCoreSchema), 'this is a bug! please report it'
2542 return schema_generator_instance.generate(cls.__pydantic_core_schema__, mode=mode)
2545def models_json_schema(
2546 models: Sequence[tuple[type[BaseModel] | type[PydanticDataclass], JsonSchemaMode]],
2547 *,
2548 by_alias: bool = True,
2549 title: str | None = None,
2550 description: str | None = None,
2551 ref_template: str = DEFAULT_REF_TEMPLATE,
2552 union_format: Literal['any_of', 'primitive_type_array'] = 'any_of',
2553 schema_generator: type[GenerateJsonSchema] = GenerateJsonSchema,
2554) -> tuple[dict[tuple[type[BaseModel] | type[PydanticDataclass], JsonSchemaMode], JsonSchemaValue], JsonSchemaValue]:
2555 """Utility function to generate a JSON Schema for multiple models.
2557 Args:
2558 models: A sequence of tuples of the form (model, mode).
2559 by_alias: Whether field aliases should be used as keys in the generated JSON Schema.
2560 title: The title of the generated JSON Schema.
2561 description: The description of the generated JSON Schema.
2562 ref_template: The reference template to use for generating JSON Schema references.
2563 union_format: The format to use when combining schemas from unions together. Can be one of:
2565 - `'any_of'`: Use the [`anyOf`](https://json-schema.org/understanding-json-schema/reference/combining#anyOf)
2566 keyword to combine schemas (the default).
2567 - `'primitive_type_array'`: Use the [`type`](https://json-schema.org/understanding-json-schema/reference/type)
2568 keyword as an array of strings, containing each type of the combination. If any of the schemas is not a primitive
2569 type (`string`, `boolean`, `null`, `integer` or `number`) or contains constraints/metadata, falls back to
2570 `any_of`.
2571 schema_generator: The schema generator to use for generating the JSON Schema.
2573 Returns:
2574 A tuple where:
2575 - The first element is a dictionary whose keys are tuples of JSON schema key type and JSON mode, and
2576 whose values are the JSON schema corresponding to that pair of inputs. (These schemas may have
2577 JsonRef references to definitions that are defined in the second returned element.)
2578 - The second element is a JSON schema containing all definitions referenced in the first returned
2579 element, along with the optional title and description keys.
2580 """
2581 for cls, _ in models:
2582 if isinstance(cls.__pydantic_core_schema__, _mock_val_ser.MockCoreSchema):
2583 cls.__pydantic_core_schema__.rebuild()
2585 instance = schema_generator(by_alias=by_alias, ref_template=ref_template, union_format=union_format)
2586 inputs: list[tuple[type[BaseModel] | type[PydanticDataclass], JsonSchemaMode, CoreSchema]] = [
2587 (m, mode, m.__pydantic_core_schema__) for m, mode in models
2588 ]
2589 json_schemas_map, definitions = instance.generate_definitions(inputs)
2591 json_schema: dict[str, Any] = {}
2592 if definitions:
2593 json_schema['$defs'] = definitions
2594 if title:
2595 json_schema['title'] = title
2596 if description:
2597 json_schema['description'] = description
2599 return json_schemas_map, json_schema
2602# ##### End JSON Schema Generation Functions #####
2605_HashableJsonValue: TypeAlias = Union[
2606 int, float, str, bool, None, tuple['_HashableJsonValue', ...], tuple[tuple[str, '_HashableJsonValue'], ...]
2607]
2610def _deduplicate_schemas(schemas: Iterable[JsonDict]) -> list[JsonDict]:
2611 return list({_make_json_hashable(schema): schema for schema in schemas}.values())
2614def _make_json_hashable(value: JsonValue) -> _HashableJsonValue:
2615 if isinstance(value, dict):
2616 return tuple(sorted((k, _make_json_hashable(v)) for k, v in value.items()))
2617 elif isinstance(value, list):
2618 return tuple(_make_json_hashable(v) for v in value)
2619 else:
2620 return value
2623@dataclasses.dataclass(**_internal_dataclass.slots_true)
2624class WithJsonSchema:
2625 """!!! abstract "Usage Documentation"
2626 [`WithJsonSchema` Annotation](../concepts/json_schema.md#withjsonschema-annotation)
2628 Add this as an annotation on a field to override the (base) JSON schema that would be generated for that field.
2629 This provides a way to set a JSON schema for types that would otherwise raise errors when producing a JSON schema,
2630 such as Callable, or types that have an is-instance core schema, without needing to go so far as creating a
2631 custom subclass of pydantic.json_schema.GenerateJsonSchema.
2632 Note that any _modifications_ to the schema that would normally be made (such as setting the title for model fields)
2633 will still be performed.
2635 If `mode` is set this will only apply to that schema generation mode, allowing you
2636 to set different json schemas for validation and serialization.
2637 """
2639 json_schema: JsonSchemaValue | None
2640 mode: Literal['validation', 'serialization'] | None = None
2642 def __get_pydantic_json_schema__(
2643 self, core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler
2644 ) -> JsonSchemaValue:
2645 mode = self.mode or handler.mode
2646 if mode != handler.mode:
2647 return handler(core_schema)
2648 if self.json_schema is None:
2649 # This exception is handled in pydantic.json_schema.GenerateJsonSchema._named_required_fields_schema
2650 raise PydanticOmit
2651 else:
2652 return self.json_schema.copy()
2654 def __hash__(self) -> int:
2655 return hash(type(self.mode))
2658class Examples:
2659 """Add examples to a JSON schema.
2661 If the JSON Schema already contains examples, the provided examples
2662 will be appended.
2664 If `mode` is set this will only apply to that schema generation mode,
2665 allowing you to add different examples for validation and serialization.
2666 """
2668 @overload
2669 @deprecated('Using a dict for `examples` is deprecated since v2.9 and will be removed in v3.0. Use a list instead.')
2670 def __init__(
2671 self, examples: dict[str, Any], mode: Literal['validation', 'serialization'] | None = None
2672 ) -> None: ...
2674 @overload
2675 def __init__(self, examples: list[Any], mode: Literal['validation', 'serialization'] | None = None) -> None: ...
2677 def __init__(
2678 self, examples: dict[str, Any] | list[Any], mode: Literal['validation', 'serialization'] | None = None
2679 ) -> None:
2680 if isinstance(examples, dict):
2681 warnings.warn(
2682 'Using a dict for `examples` is deprecated, use a list instead.',
2683 PydanticDeprecatedSince29,
2684 stacklevel=2,
2685 )
2686 self.examples = examples
2687 self.mode = mode
2689 def __get_pydantic_json_schema__(
2690 self, core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler
2691 ) -> JsonSchemaValue:
2692 mode = self.mode or handler.mode
2693 json_schema = handler(core_schema)
2694 if mode != handler.mode:
2695 return json_schema
2696 examples = json_schema.get('examples')
2697 if examples is None:
2698 json_schema['examples'] = to_jsonable_python(self.examples)
2699 if isinstance(examples, dict):
2700 if isinstance(self.examples, list):
2701 warnings.warn(
2702 'Updating existing JSON Schema examples of type dict with examples of type list. '
2703 'Only the existing examples values will be retained. Note that dict support for '
2704 'examples is deprecated and will be removed in v3.0.',
2705 UserWarning,
2706 )
2707 json_schema['examples'] = to_jsonable_python(
2708 [ex for value in examples.values() for ex in value] + self.examples
2709 )
2710 else:
2711 json_schema['examples'] = to_jsonable_python({**examples, **self.examples})
2712 if isinstance(examples, list):
2713 if isinstance(self.examples, list):
2714 json_schema['examples'] = to_jsonable_python(examples + self.examples)
2715 elif isinstance(self.examples, dict):
2716 warnings.warn(
2717 'Updating existing JSON Schema examples of type list with examples of type dict. '
2718 'Only the examples values will be retained. Note that dict support for '
2719 'examples is deprecated and will be removed in v3.0.',
2720 UserWarning,
2721 )
2722 json_schema['examples'] = to_jsonable_python(
2723 examples + [ex for value in self.examples.values() for ex in value]
2724 )
2726 return json_schema
2728 def __hash__(self) -> int:
2729 return hash(type(self.mode))
2732def _get_all_json_refs(item: Any) -> set[JsonRef]:
2733 """Get all the definitions references from a JSON schema."""
2734 refs: set[JsonRef] = set()
2735 stack = [item]
2737 while stack:
2738 current = stack.pop()
2739 if isinstance(current, dict):
2740 for key, value in current.items():
2741 if key == 'examples' and isinstance(value, list):
2742 # Skip examples that may contain arbitrary values and references
2743 # (e.g. `{"examples": [{"$ref": "..."}]}`). Note: checking for value
2744 # of type list is necessary to avoid skipping valid portions of the schema,
2745 # for instance when "examples" is used as a property key. A more robust solution
2746 # could be found, but would require more advanced JSON Schema parsing logic.
2747 continue
2748 if key == '$ref' and isinstance(value, str):
2749 refs.add(JsonRef(value))
2750 elif isinstance(value, dict):
2751 stack.append(value)
2752 elif isinstance(value, list):
2753 stack.extend(value)
2754 elif isinstance(current, list):
2755 stack.extend(current)
2757 return refs
2760AnyType = TypeVar('AnyType')
2762if TYPE_CHECKING:
2763 SkipJsonSchema = Annotated[AnyType, ...]
2764else:
2766 @dataclasses.dataclass(**_internal_dataclass.slots_true)
2767 class SkipJsonSchema:
2768 """!!! abstract "Usage Documentation"
2769 [`SkipJsonSchema` Annotation](../concepts/json_schema.md#skipjsonschema-annotation)
2771 Add this as an annotation on a field to skip generating a JSON schema for that field.
2773 Example:
2774 ```python
2775 from pprint import pprint
2776 from typing import Union
2778 from pydantic import BaseModel
2779 from pydantic.json_schema import SkipJsonSchema
2781 class Model(BaseModel):
2782 a: Union[int, None] = None # (1)!
2783 b: Union[int, SkipJsonSchema[None]] = None # (2)!
2784 c: SkipJsonSchema[Union[int, None]] = None # (3)!
2786 pprint(Model.model_json_schema())
2787 '''
2788 {
2789 'properties': {
2790 'a': {
2791 'anyOf': [
2792 {'type': 'integer'},
2793 {'type': 'null'}
2794 ],
2795 'default': None,
2796 'title': 'A'
2797 },
2798 'b': {
2799 'default': None,
2800 'title': 'B',
2801 'type': 'integer'
2802 }
2803 },
2804 'title': 'Model',
2805 'type': 'object'
2806 }
2807 '''
2808 ```
2810 1. The integer and null types are both included in the schema for `a`.
2811 2. The integer type is the only type included in the schema for `b`.
2812 3. The entirety of the `c` field is omitted from the schema.
2813 """
2815 def __class_getitem__(cls, item: AnyType) -> AnyType:
2816 return Annotated[item, cls()]
2818 def __get_pydantic_json_schema__(
2819 self, core_schema: CoreSchema, handler: GetJsonSchemaHandler
2820 ) -> JsonSchemaValue:
2821 raise PydanticOmit
2823 def __hash__(self) -> int:
2824 return hash(type(self))
2827def _get_typed_dict_config(cls: type[Any] | None) -> ConfigDict:
2828 if cls is not None:
2829 try:
2830 return _decorators.get_attribute_from_bases(cls, '__pydantic_config__')
2831 except AttributeError:
2832 pass
2833 return {}
2836def _get_ser_schema_for_default_value(schema: CoreSchema) -> core_schema.PlainSerializerFunctionSerSchema | None:
2837 """Get a `'function-plain'` serialization schema that can be used to serialize a default value.
2839 This takes into account having the serialization schema nested under validation schema(s).
2840 """
2841 if (
2842 (ser_schema := schema.get('serialization'))
2843 and ser_schema['type'] == 'function-plain'
2844 and not ser_schema.get('info_arg')
2845 ):
2846 return ser_schema
2847 if _core_utils.is_function_with_inner_schema(schema):
2848 return _get_ser_schema_for_default_value(schema['schema'])