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 field['type'] == 'typed-dict-field':
1760 required = field.get('required', total)
1761 else:
1762 required = field['schema']['type'] != 'default'
1764 if self.mode == 'serialization':
1765 has_exclude_if = field.get('serialization_exclude_if') is not None
1766 if self._config.json_schema_serialization_defaults_required:
1767 return not has_exclude_if
1768 else:
1769 return required and not has_exclude_if
1770 else:
1771 return required
1773 def dataclass_args_schema(self, schema: core_schema.DataclassArgsSchema) -> JsonSchemaValue:
1774 """Generates a JSON schema that matches a schema that defines a dataclass's constructor arguments.
1776 Args:
1777 schema: The core schema.
1779 Returns:
1780 The generated JSON schema.
1781 """
1782 named_required_fields: list[tuple[str, bool, CoreSchemaField]] = [
1783 (field['name'], self.field_is_required(field, total=True), field)
1784 for field in schema['fields']
1785 if self.field_is_present(field)
1786 ]
1787 if self.mode == 'serialization':
1788 named_required_fields.extend(self._name_required_computed_fields(schema.get('computed_fields', [])))
1789 return self._named_required_fields_schema(named_required_fields)
1791 def dataclass_schema(self, schema: core_schema.DataclassSchema) -> JsonSchemaValue:
1792 """Generates a JSON schema that matches a schema that defines a dataclass.
1794 Args:
1795 schema: The core schema.
1797 Returns:
1798 The generated JSON schema.
1799 """
1800 from ._internal._dataclasses import is_stdlib_dataclass
1802 cls = schema['cls']
1803 config: ConfigDict = getattr(cls, '__pydantic_config__', cast('ConfigDict', {}))
1805 with self._config_wrapper_stack.push(config):
1806 json_schema = self.generate_inner(schema['schema']).copy()
1808 self._update_class_schema(json_schema, cls, config)
1810 # Dataclass-specific handling of description
1811 if is_stdlib_dataclass(cls):
1812 # vanilla dataclass; don't use cls.__doc__ as it will contain the class signature by default
1813 description = None
1814 else:
1815 description = None if cls.__doc__ is None else inspect.cleandoc(cls.__doc__)
1816 if description:
1817 json_schema['description'] = description
1819 return json_schema
1821 def arguments_schema(self, schema: core_schema.ArgumentsSchema) -> JsonSchemaValue:
1822 """Generates a JSON schema that matches a schema that defines a function's arguments.
1824 Args:
1825 schema: The core schema.
1827 Returns:
1828 The generated JSON schema.
1829 """
1830 prefer_positional = schema.get('metadata', {}).get('pydantic_js_prefer_positional_arguments')
1832 arguments = schema['arguments_schema']
1833 kw_only_arguments = [a for a in arguments if a.get('mode') == 'keyword_only']
1834 kw_or_p_arguments = [a for a in arguments if a.get('mode') in {'positional_or_keyword', None}]
1835 p_only_arguments = [a for a in arguments if a.get('mode') == 'positional_only']
1836 var_args_schema = schema.get('var_args_schema')
1837 var_kwargs_schema = schema.get('var_kwargs_schema')
1839 if prefer_positional:
1840 positional_possible = not kw_only_arguments and not var_kwargs_schema
1841 if positional_possible:
1842 return self.p_arguments_schema(p_only_arguments + kw_or_p_arguments, var_args_schema)
1844 keyword_possible = not p_only_arguments and not var_args_schema
1845 if keyword_possible:
1846 return self.kw_arguments_schema(kw_or_p_arguments + kw_only_arguments, var_kwargs_schema)
1848 if not prefer_positional:
1849 positional_possible = not kw_only_arguments and not var_kwargs_schema
1850 if positional_possible:
1851 return self.p_arguments_schema(p_only_arguments + kw_or_p_arguments, var_args_schema)
1853 raise PydanticInvalidForJsonSchema(
1854 'Unable to generate JSON schema for arguments validator with positional-only and keyword-only arguments'
1855 )
1857 def kw_arguments_schema(
1858 self, arguments: list[core_schema.ArgumentsParameter], var_kwargs_schema: CoreSchema | None
1859 ) -> JsonSchemaValue:
1860 """Generates a JSON schema that matches a schema that defines a function's keyword arguments.
1862 Args:
1863 arguments: The core schema.
1865 Returns:
1866 The generated JSON schema.
1867 """
1868 properties: dict[str, JsonSchemaValue] = {}
1869 required: list[str] = []
1870 for argument in arguments:
1871 name = self.get_argument_name(argument)
1872 argument_schema = self.generate_inner(argument['schema']).copy()
1873 if 'title' not in argument_schema and self.field_title_should_be_set(argument['schema']):
1874 argument_schema['title'] = self.get_title_from_name(name)
1875 properties[name] = argument_schema
1877 if argument['schema']['type'] != 'default':
1878 # This assumes that if the argument has a default value,
1879 # the inner schema must be of type WithDefaultSchema.
1880 # I believe this is true, but I am not 100% sure
1881 required.append(name)
1883 json_schema: JsonSchemaValue = {'type': 'object', 'properties': properties}
1884 if required:
1885 json_schema['required'] = required
1887 if var_kwargs_schema:
1888 additional_properties_schema = self.generate_inner(var_kwargs_schema)
1889 if additional_properties_schema:
1890 json_schema['additionalProperties'] = additional_properties_schema
1891 else:
1892 json_schema['additionalProperties'] = False
1893 return json_schema
1895 def p_arguments_schema(
1896 self, arguments: list[core_schema.ArgumentsParameter], var_args_schema: CoreSchema | None
1897 ) -> JsonSchemaValue:
1898 """Generates a JSON schema that matches a schema that defines a function's positional arguments.
1900 Args:
1901 arguments: The core schema.
1903 Returns:
1904 The generated JSON schema.
1905 """
1906 prefix_items: list[JsonSchemaValue] = []
1907 min_items = 0
1909 for argument in arguments:
1910 name = self.get_argument_name(argument)
1912 argument_schema = self.generate_inner(argument['schema']).copy()
1913 if 'title' not in argument_schema and self.field_title_should_be_set(argument['schema']):
1914 argument_schema['title'] = self.get_title_from_name(name)
1915 prefix_items.append(argument_schema)
1917 if argument['schema']['type'] != 'default':
1918 # This assumes that if the argument has a default value,
1919 # the inner schema must be of type WithDefaultSchema.
1920 # I believe this is true, but I am not 100% sure
1921 min_items += 1
1923 json_schema: JsonSchemaValue = {'type': 'array'}
1924 if prefix_items:
1925 json_schema['prefixItems'] = prefix_items
1926 if min_items:
1927 json_schema['minItems'] = min_items
1929 if var_args_schema:
1930 items_schema = self.generate_inner(var_args_schema)
1931 if items_schema:
1932 json_schema['items'] = items_schema
1933 else:
1934 json_schema['maxItems'] = len(prefix_items)
1936 return json_schema
1938 def get_argument_name(self, argument: core_schema.ArgumentsParameter | core_schema.ArgumentsV3Parameter) -> str:
1939 """Retrieves the name of an argument.
1941 Args:
1942 argument: The core schema.
1944 Returns:
1945 The name of the argument.
1946 """
1947 name = argument['name']
1948 if self.by_alias:
1949 alias = argument.get('alias')
1950 if isinstance(alias, str):
1951 name = alias
1952 else:
1953 pass # might want to do something else?
1954 return name
1956 def arguments_v3_schema(self, schema: core_schema.ArgumentsV3Schema) -> JsonSchemaValue:
1957 """Generates a JSON schema that matches a schema that defines a function's arguments.
1959 Args:
1960 schema: The core schema.
1962 Returns:
1963 The generated JSON schema.
1964 """
1965 arguments = schema['arguments_schema']
1966 properties: dict[str, JsonSchemaValue] = {}
1967 required: list[str] = []
1968 for argument in arguments:
1969 mode = argument.get('mode', 'positional_or_keyword')
1970 name = self.get_argument_name(argument)
1971 argument_schema = self.generate_inner(argument['schema']).copy()
1972 if mode == 'var_args':
1973 argument_schema = {'type': 'array', 'items': argument_schema}
1974 elif mode == 'var_kwargs_uniform':
1975 argument_schema = {'type': 'object', 'additionalProperties': argument_schema}
1977 argument_schema.setdefault('title', self.get_title_from_name(name))
1978 properties[name] = argument_schema
1980 if (
1981 (mode == 'var_kwargs_unpacked_typed_dict' and 'required' in argument_schema)
1982 or mode not in {'var_args', 'var_kwargs_uniform', 'var_kwargs_unpacked_typed_dict'}
1983 and argument['schema']['type'] != 'default'
1984 ):
1985 # This assumes that if the argument has a default value,
1986 # the inner schema must be of type WithDefaultSchema.
1987 # I believe this is true, but I am not 100% sure
1988 required.append(name)
1990 json_schema: JsonSchemaValue = {'type': 'object', 'properties': properties}
1991 if required:
1992 json_schema['required'] = required
1993 return json_schema
1995 def call_schema(self, schema: core_schema.CallSchema) -> JsonSchemaValue:
1996 """Generates a JSON schema that matches a schema that defines a function call.
1998 Args:
1999 schema: The core schema.
2001 Returns:
2002 The generated JSON schema.
2003 """
2004 return self.generate_inner(schema['arguments_schema'])
2006 def custom_error_schema(self, schema: core_schema.CustomErrorSchema) -> JsonSchemaValue:
2007 """Generates a JSON schema that matches a schema that defines a custom error.
2009 Args:
2010 schema: The core schema.
2012 Returns:
2013 The generated JSON schema.
2014 """
2015 return self.generate_inner(schema['schema'])
2017 def json_schema(self, schema: core_schema.JsonSchema) -> JsonSchemaValue:
2018 """Generates a JSON schema that matches a schema that defines a JSON object.
2020 Args:
2021 schema: The core schema.
2023 Returns:
2024 The generated JSON schema.
2025 """
2026 content_core_schema = schema.get('schema') or core_schema.any_schema()
2027 content_json_schema = self.generate_inner(content_core_schema)
2028 if self.mode == 'validation':
2029 return {'type': 'string', 'contentMediaType': 'application/json', 'contentSchema': content_json_schema}
2030 else:
2031 # self.mode == 'serialization'
2032 return content_json_schema
2034 def url_schema(self, schema: core_schema.UrlSchema) -> JsonSchemaValue:
2035 """Generates a JSON schema that matches a schema that defines a URL.
2037 Args:
2038 schema: The core schema.
2040 Returns:
2041 The generated JSON schema.
2042 """
2043 json_schema = {'type': 'string', 'format': 'uri', 'minLength': 1}
2044 self.update_with_validations(json_schema, schema, self.ValidationsMapping.string)
2045 return json_schema
2047 def multi_host_url_schema(self, schema: core_schema.MultiHostUrlSchema) -> JsonSchemaValue:
2048 """Generates a JSON schema that matches a schema that defines a URL that can be used with multiple hosts.
2050 Args:
2051 schema: The core schema.
2053 Returns:
2054 The generated JSON schema.
2055 """
2056 # Note: 'multi-host-uri' is a custom/pydantic-specific format, not part of the JSON Schema spec
2057 json_schema = {'type': 'string', 'format': 'multi-host-uri', 'minLength': 1}
2058 self.update_with_validations(json_schema, schema, self.ValidationsMapping.string)
2059 return json_schema
2061 def uuid_schema(self, schema: core_schema.UuidSchema) -> JsonSchemaValue:
2062 """Generates a JSON schema that matches a UUID.
2064 Args:
2065 schema: The core schema.
2067 Returns:
2068 The generated JSON schema.
2069 """
2070 return {'type': 'string', 'format': 'uuid'}
2072 def definitions_schema(self, schema: core_schema.DefinitionsSchema) -> JsonSchemaValue:
2073 """Generates a JSON schema that matches a schema that defines a JSON object with definitions.
2075 Args:
2076 schema: The core schema.
2078 Returns:
2079 The generated JSON schema.
2080 """
2081 for definition in schema['definitions']:
2082 try:
2083 self.generate_inner(definition)
2084 except PydanticInvalidForJsonSchema as e: # noqa: PERF203
2085 core_ref: CoreRef = CoreRef(definition['ref']) # type: ignore
2086 self._core_defs_invalid_for_json_schema[self.get_defs_ref((core_ref, self.mode))] = e
2087 continue
2088 return self.generate_inner(schema['schema'])
2090 def definition_ref_schema(self, schema: core_schema.DefinitionReferenceSchema) -> JsonSchemaValue:
2091 """Generates a JSON schema that matches a schema that references a definition.
2093 Args:
2094 schema: The core schema.
2096 Returns:
2097 The generated JSON schema.
2098 """
2099 core_ref = CoreRef(schema['schema_ref'])
2100 _, ref_json_schema = self.get_cache_defs_ref_schema(core_ref)
2101 return ref_json_schema
2103 def ser_schema(
2104 self, schema: core_schema.SerSchema | core_schema.IncExSeqSerSchema | core_schema.IncExDictSerSchema
2105 ) -> JsonSchemaValue | None:
2106 """Generates a JSON schema that matches a schema that defines a serialized object.
2108 Args:
2109 schema: The core schema.
2111 Returns:
2112 The generated JSON schema.
2113 """
2114 schema_type = schema['type']
2115 if schema_type == 'function-plain' or schema_type == 'function-wrap':
2116 # PlainSerializerFunctionSerSchema or WrapSerializerFunctionSerSchema
2117 return_schema = schema.get('return_schema')
2118 if return_schema is not None:
2119 return self.generate_inner(return_schema)
2120 elif schema_type == 'format' or schema_type == 'to-string':
2121 # FormatSerSchema or ToStringSerSchema
2122 return self.str_schema(core_schema.str_schema())
2123 elif schema['type'] == 'model':
2124 # ModelSerSchema
2125 return self.generate_inner(schema['schema'])
2126 return None
2128 def complex_schema(self, schema: core_schema.ComplexSchema) -> JsonSchemaValue:
2129 """Generates a JSON schema that matches a complex number.
2131 JSON has no standard way to represent complex numbers. Complex number is not a numeric
2132 type. Here we represent complex number as strings following the rule defined by Python.
2133 For instance, '1+2j' is an accepted complex string. Details can be found in
2134 [Python's `complex` documentation][complex].
2136 Args:
2137 schema: The core schema.
2139 Returns:
2140 The generated JSON schema.
2141 """
2142 return {'type': 'string'}
2144 # ### Utility methods
2146 def get_title_from_name(self, name: str) -> str:
2147 """Retrieves a title from a name.
2149 Args:
2150 name: The name to retrieve a title from.
2152 Returns:
2153 The title.
2154 """
2155 return name.title().replace('_', ' ').strip()
2157 def field_title_should_be_set(self, schema: CoreSchemaOrField) -> bool:
2158 """Returns true if a field with the given schema should have a title set based on the field name.
2160 Intuitively, we want this to return true for schemas that wouldn't otherwise provide their own title
2161 (e.g., int, float, str), and false for those that would (e.g., BaseModel subclasses).
2163 Args:
2164 schema: The schema to check.
2166 Returns:
2167 `True` if the field should have a title set, `False` otherwise.
2168 """
2169 if _core_utils.is_core_schema_field(schema):
2170 if schema['type'] == 'computed-field':
2171 field_schema = schema['return_schema']
2172 else:
2173 field_schema = schema['schema']
2174 return self.field_title_should_be_set(field_schema)
2176 elif _core_utils.is_core_schema(schema):
2177 if schema.get('ref'): # things with refs, such as models and enums, should not have titles set
2178 return False
2179 if schema['type'] in {'default', 'nullable', 'definitions'}:
2180 return self.field_title_should_be_set(schema['schema']) # type: ignore[typeddict-item]
2181 if _core_utils.is_function_with_inner_schema(schema):
2182 return self.field_title_should_be_set(schema['schema'])
2183 if schema['type'] == 'definition-ref':
2184 # Referenced schemas should not have titles set for the same reason
2185 # schemas with refs should not
2186 return False
2187 return True # anything else should have title set
2189 else:
2190 raise PydanticInvalidForJsonSchema(f'Unexpected schema type: schema={schema}') # pragma: no cover
2192 def normalize_name(self, name: str) -> str:
2193 """Normalizes a name to be used as a key in a dictionary.
2195 Args:
2196 name: The name to normalize.
2198 Returns:
2199 The normalized name.
2200 """
2201 return re.sub(r'[^a-zA-Z0-9.\-_]', '_', name).replace('.', '__')
2203 def get_defs_ref(self, core_mode_ref: CoreModeRef) -> DefsRef:
2204 """Override this method to change the way that definitions keys are generated from a core reference.
2206 Args:
2207 core_mode_ref: The core reference.
2209 Returns:
2210 The definitions key.
2211 """
2212 # Split the core ref into "components"; generic origins and arguments are each separate components
2213 core_ref, mode = core_mode_ref
2214 components = re.split(r'([\][,])', core_ref)
2215 # Remove IDs from each component
2216 components = [x.rsplit(':', 1)[0] for x in components]
2217 core_ref_no_id = ''.join(components)
2218 # Remove everything before the last period from each "component"
2219 components = [re.sub(r'(?:[^.[\]]+\.)+((?:[^.[\]]+))', r'\1', x) for x in components]
2220 short_ref = ''.join(components)
2222 mode_title = _MODE_TITLE_MAPPING[mode]
2224 # It is important that the generated defs_ref values be such that at least one choice will not
2225 # be generated for any other core_ref. Currently, this should be the case because we include
2226 # the id of the source type in the core_ref
2227 name = DefsRef(self.normalize_name(short_ref))
2228 name_mode = DefsRef(self.normalize_name(short_ref) + f'-{mode_title}')
2229 module_qualname = DefsRef(self.normalize_name(core_ref_no_id))
2230 module_qualname_mode = DefsRef(f'{module_qualname}-{mode_title}')
2231 module_qualname_id = DefsRef(self.normalize_name(core_ref))
2232 occurrence_index = self._collision_index.get(module_qualname_id)
2233 if occurrence_index is None:
2234 self._collision_counter[module_qualname] += 1
2235 occurrence_index = self._collision_index[module_qualname_id] = self._collision_counter[module_qualname]
2237 module_qualname_occurrence = DefsRef(f'{module_qualname}__{occurrence_index}')
2238 module_qualname_occurrence_mode = DefsRef(f'{module_qualname_mode}__{occurrence_index}')
2240 self._prioritized_defsref_choices[module_qualname_occurrence_mode] = [
2241 name,
2242 name_mode,
2243 module_qualname,
2244 module_qualname_mode,
2245 module_qualname_occurrence,
2246 module_qualname_occurrence_mode,
2247 ]
2249 return module_qualname_occurrence_mode
2251 def get_cache_defs_ref_schema(self, core_ref: CoreRef) -> tuple[DefsRef, JsonSchemaValue]:
2252 """This method wraps the get_defs_ref method with some cache-lookup/population logic,
2253 and returns both the produced defs_ref and the JSON schema that will refer to the right definition.
2255 Args:
2256 core_ref: The core reference to get the definitions reference for.
2258 Returns:
2259 A tuple of the definitions reference and the JSON schema that will refer to it.
2260 """
2261 core_mode_ref = (core_ref, self.mode)
2262 maybe_defs_ref = self.core_to_defs_refs.get(core_mode_ref)
2263 if maybe_defs_ref is not None:
2264 json_ref = self.core_to_json_refs[core_mode_ref]
2265 return maybe_defs_ref, {'$ref': json_ref}
2267 defs_ref = self.get_defs_ref(core_mode_ref)
2269 # populate the ref translation mappings
2270 self.core_to_defs_refs[core_mode_ref] = defs_ref
2271 self.defs_to_core_refs[defs_ref] = core_mode_ref
2273 json_ref = JsonRef(self.ref_template.format(model=defs_ref))
2274 self.core_to_json_refs[core_mode_ref] = json_ref
2275 self.json_to_defs_refs[json_ref] = defs_ref
2276 ref_json_schema = {'$ref': json_ref}
2277 return defs_ref, ref_json_schema
2279 def handle_ref_overrides(self, json_schema: JsonSchemaValue) -> JsonSchemaValue:
2280 """Remove any sibling keys that are redundant with the referenced schema.
2282 Args:
2283 json_schema: The schema to remove redundant sibling keys from.
2285 Returns:
2286 The schema with redundant sibling keys removed.
2287 """
2288 if '$ref' in json_schema:
2289 # prevent modifications to the input; this copy may be safe to drop if there is significant overhead
2290 json_schema = json_schema.copy()
2292 referenced_json_schema = self.get_schema_from_definitions(JsonRef(json_schema['$ref']))
2293 if referenced_json_schema is None:
2294 # This can happen when building schemas for models with not-yet-defined references.
2295 # It may be a good idea to do a recursive pass at the end of the generation to remove
2296 # any redundant override keys.
2297 return json_schema
2298 for k, v in list(json_schema.items()):
2299 if k == '$ref':
2300 continue
2301 if k in referenced_json_schema and referenced_json_schema[k] == v:
2302 del json_schema[k] # redundant key
2304 return json_schema
2306 def get_schema_from_definitions(self, json_ref: JsonRef) -> JsonSchemaValue | None:
2307 try:
2308 def_ref = self.json_to_defs_refs[json_ref]
2309 if def_ref in self._core_defs_invalid_for_json_schema:
2310 raise self._core_defs_invalid_for_json_schema[def_ref]
2311 return self.definitions.get(def_ref, None)
2312 except KeyError:
2313 if json_ref.startswith(('http://', 'https://')):
2314 return None
2315 raise
2317 def encode_default(self, dft: Any) -> Any:
2318 """Encode a default value to a JSON-serializable value.
2320 This is used to encode default values for fields in the generated JSON schema.
2322 Args:
2323 dft: The default value to encode.
2325 Returns:
2326 The encoded default value.
2327 """
2328 from .type_adapter import TypeAdapter, _type_has_config
2330 config = self._config
2331 try:
2332 default = (
2333 dft
2334 if _type_has_config(type(dft))
2335 else TypeAdapter(type(dft), config=config.config_dict).dump_python(
2336 dft, by_alias=self.by_alias, mode='json'
2337 )
2338 )
2339 except PydanticSchemaGenerationError:
2340 raise pydantic_core.PydanticSerializationError(f'Unable to encode default value {dft}')
2342 return pydantic_core.to_jsonable_python(
2343 default, timedelta_mode=config.ser_json_timedelta, bytes_mode=config.ser_json_bytes, by_alias=self.by_alias
2344 )
2346 def update_with_validations(
2347 self, json_schema: JsonSchemaValue, core_schema: CoreSchema, mapping: dict[str, str]
2348 ) -> None:
2349 """Update the json_schema with the corresponding validations specified in the core_schema,
2350 using the provided mapping to translate keys in core_schema to the appropriate keys for a JSON schema.
2352 Args:
2353 json_schema: The JSON schema to update.
2354 core_schema: The core schema to get the validations from.
2355 mapping: A mapping from core_schema attribute names to the corresponding JSON schema attribute names.
2356 """
2357 for core_key, json_schema_key in mapping.items():
2358 if core_key in core_schema:
2359 json_schema[json_schema_key] = core_schema[core_key]
2361 class ValidationsMapping:
2362 """This class just contains mappings from core_schema attribute names to the corresponding
2363 JSON schema attribute names. While I suspect it is unlikely to be necessary, you can in
2364 principle override this class in a subclass of GenerateJsonSchema (by inheriting from
2365 GenerateJsonSchema.ValidationsMapping) to change these mappings.
2366 """
2368 numeric = {
2369 'multiple_of': 'multipleOf',
2370 'le': 'maximum',
2371 'ge': 'minimum',
2372 'lt': 'exclusiveMaximum',
2373 'gt': 'exclusiveMinimum',
2374 }
2375 bytes = {
2376 'min_length': 'minLength',
2377 'max_length': 'maxLength',
2378 }
2379 string = {
2380 'min_length': 'minLength',
2381 'max_length': 'maxLength',
2382 'pattern': 'pattern',
2383 }
2384 array = {
2385 'min_length': 'minItems',
2386 'max_length': 'maxItems',
2387 }
2388 object = {
2389 'min_length': 'minProperties',
2390 'max_length': 'maxProperties',
2391 }
2393 def get_flattened_anyof(self, schemas: list[JsonSchemaValue]) -> JsonSchemaValue:
2394 members = []
2395 for schema in schemas:
2396 if len(schema) == 1 and 'anyOf' in schema:
2397 members.extend(schema['anyOf'])
2398 else:
2399 members.append(schema)
2400 members = _deduplicate_schemas(members)
2401 if len(members) == 1:
2402 return members[0]
2403 return {'anyOf': members}
2405 def get_json_ref_counts(self, json_schema: JsonSchemaValue) -> dict[JsonRef, int]:
2406 """Get all values corresponding to the key '$ref' anywhere in the json_schema."""
2407 json_refs: dict[JsonRef, int] = Counter()
2409 def _add_json_refs(schema: Any) -> None:
2410 if isinstance(schema, dict):
2411 if '$ref' in schema:
2412 json_ref = JsonRef(schema['$ref'])
2413 if not isinstance(json_ref, str):
2414 return # in this case, '$ref' might have been the name of a property
2415 already_visited = json_ref in json_refs
2416 json_refs[json_ref] += 1
2417 if already_visited:
2418 return # prevent recursion on a definition that was already visited
2419 try:
2420 defs_ref = self.json_to_defs_refs[json_ref]
2421 if defs_ref in self._core_defs_invalid_for_json_schema:
2422 raise self._core_defs_invalid_for_json_schema[defs_ref]
2423 _add_json_refs(self.definitions[defs_ref])
2424 except KeyError:
2425 if not json_ref.startswith(('http://', 'https://')):
2426 raise
2428 for k, v in schema.items():
2429 if k == 'examples' and isinstance(v, list):
2430 # Skip examples that may contain arbitrary values and references
2431 # (see the comment in `_get_all_json_refs` for more details).
2432 continue
2433 _add_json_refs(v)
2434 elif isinstance(schema, list):
2435 for v in schema:
2436 _add_json_refs(v)
2438 _add_json_refs(json_schema)
2439 return json_refs
2441 def handle_invalid_for_json_schema(self, schema: CoreSchemaOrField, error_info: str) -> JsonSchemaValue:
2442 raise PydanticInvalidForJsonSchema(f'Cannot generate a JsonSchema for {error_info}')
2444 def emit_warning(self, kind: JsonSchemaWarningKind, detail: str) -> None:
2445 """This method simply emits PydanticJsonSchemaWarnings based on handling in the `warning_message` method."""
2446 message = self.render_warning_message(kind, detail)
2447 if message is not None:
2448 warnings.warn(message, PydanticJsonSchemaWarning)
2450 def render_warning_message(self, kind: JsonSchemaWarningKind, detail: str) -> str | None:
2451 """This method is responsible for ignoring warnings as desired, and for formatting the warning messages.
2453 You can override the value of `ignored_warning_kinds` in a subclass of GenerateJsonSchema
2454 to modify what warnings are generated. If you want more control, you can override this method;
2455 just return None in situations where you don't want warnings to be emitted.
2457 Args:
2458 kind: The kind of warning to render. It can be one of the following:
2460 - 'skipped-choice': A choice field was skipped because it had no valid choices.
2461 - 'non-serializable-default': A default value was skipped because it was not JSON-serializable.
2462 detail: A string with additional details about the warning.
2464 Returns:
2465 The formatted warning message, or `None` if no warning should be emitted.
2466 """
2467 if kind in self.ignored_warning_kinds:
2468 return None
2469 return f'{detail} [{kind}]'
2471 def _build_definitions_remapping(self) -> _DefinitionsRemapping:
2472 defs_to_json: dict[DefsRef, JsonRef] = {}
2473 for defs_refs in self._prioritized_defsref_choices.values():
2474 for defs_ref in defs_refs:
2475 json_ref = JsonRef(self.ref_template.format(model=defs_ref))
2476 defs_to_json[defs_ref] = json_ref
2478 return _DefinitionsRemapping.from_prioritized_choices(
2479 self._prioritized_defsref_choices, defs_to_json, self.definitions
2480 )
2482 def _garbage_collect_definitions(self, schema: JsonSchemaValue) -> None:
2483 visited_defs_refs: set[DefsRef] = set()
2484 unvisited_json_refs = _get_all_json_refs(schema)
2485 while unvisited_json_refs:
2486 next_json_ref = unvisited_json_refs.pop()
2487 try:
2488 next_defs_ref = self.json_to_defs_refs[next_json_ref]
2489 if next_defs_ref in visited_defs_refs:
2490 continue
2491 visited_defs_refs.add(next_defs_ref)
2492 unvisited_json_refs.update(_get_all_json_refs(self.definitions[next_defs_ref]))
2493 except KeyError:
2494 if not next_json_ref.startswith(('http://', 'https://')):
2495 raise
2497 self.definitions = {k: v for k, v in self.definitions.items() if k in visited_defs_refs}
2500# ##### Start JSON Schema Generation Functions #####
2503def model_json_schema(
2504 cls: type[BaseModel] | type[PydanticDataclass],
2505 by_alias: bool = True,
2506 ref_template: str = DEFAULT_REF_TEMPLATE,
2507 union_format: Literal['any_of', 'primitive_type_array'] = 'any_of',
2508 schema_generator: type[GenerateJsonSchema] = GenerateJsonSchema,
2509 mode: JsonSchemaMode = 'validation',
2510) -> dict[str, Any]:
2511 """Utility function to generate a JSON Schema for a model.
2513 Args:
2514 cls: The model class to generate a JSON Schema for.
2515 by_alias: If `True` (the default), fields will be serialized according to their alias.
2516 If `False`, fields will be serialized according to their attribute name.
2517 ref_template: The template to use for generating JSON Schema references.
2518 union_format: The format to use when combining schemas from unions together. Can be one of:
2520 - `'any_of'`: Use the [`anyOf`](https://json-schema.org/understanding-json-schema/reference/combining#anyOf)
2521 keyword to combine schemas (the default).
2522 - `'primitive_type_array'`: Use the [`type`](https://json-schema.org/understanding-json-schema/reference/type)
2523 keyword as an array of strings, containing each type of the combination. If any of the schemas is not a primitive
2524 type (`string`, `boolean`, `null`, `integer` or `number`) or contains constraints/metadata, falls back to
2525 `any_of`.
2526 schema_generator: The class to use for generating the JSON Schema.
2527 mode: The mode to use for generating the JSON Schema. It can be one of the following:
2529 - 'validation': Generate a JSON Schema for validating data.
2530 - 'serialization': Generate a JSON Schema for serializing data.
2532 Returns:
2533 The generated JSON Schema.
2534 """
2535 from .main import BaseModel
2537 schema_generator_instance = schema_generator(
2538 by_alias=by_alias, ref_template=ref_template, union_format=union_format
2539 )
2541 if isinstance(cls.__pydantic_core_schema__, _mock_val_ser.MockCoreSchema):
2542 cls.__pydantic_core_schema__.rebuild()
2544 if cls is BaseModel:
2545 raise AttributeError('model_json_schema() must be called on a subclass of BaseModel, not BaseModel itself.')
2547 assert not isinstance(cls.__pydantic_core_schema__, _mock_val_ser.MockCoreSchema), 'this is a bug! please report it'
2548 return schema_generator_instance.generate(cls.__pydantic_core_schema__, mode=mode)
2551def models_json_schema(
2552 models: Sequence[tuple[type[BaseModel] | type[PydanticDataclass], JsonSchemaMode]],
2553 *,
2554 by_alias: bool = True,
2555 title: str | None = None,
2556 description: str | None = None,
2557 ref_template: str = DEFAULT_REF_TEMPLATE,
2558 union_format: Literal['any_of', 'primitive_type_array'] = 'any_of',
2559 schema_generator: type[GenerateJsonSchema] = GenerateJsonSchema,
2560) -> tuple[dict[tuple[type[BaseModel] | type[PydanticDataclass], JsonSchemaMode], JsonSchemaValue], JsonSchemaValue]:
2561 """Utility function to generate a JSON Schema for multiple models.
2563 Args:
2564 models: A sequence of tuples of the form (model, mode).
2565 by_alias: Whether field aliases should be used as keys in the generated JSON Schema.
2566 title: The title of the generated JSON Schema.
2567 description: The description of the generated JSON Schema.
2568 ref_template: The reference template to use for generating JSON Schema references.
2569 union_format: The format to use when combining schemas from unions together. Can be one of:
2571 - `'any_of'`: Use the [`anyOf`](https://json-schema.org/understanding-json-schema/reference/combining#anyOf)
2572 keyword to combine schemas (the default).
2573 - `'primitive_type_array'`: Use the [`type`](https://json-schema.org/understanding-json-schema/reference/type)
2574 keyword as an array of strings, containing each type of the combination. If any of the schemas is not a primitive
2575 type (`string`, `boolean`, `null`, `integer` or `number`) or contains constraints/metadata, falls back to
2576 `any_of`.
2577 schema_generator: The schema generator to use for generating the JSON Schema.
2579 Returns:
2580 A tuple where:
2581 - The first element is a dictionary whose keys are tuples of JSON schema key type and JSON mode, and
2582 whose values are the JSON schema corresponding to that pair of inputs. (These schemas may have
2583 JsonRef references to definitions that are defined in the second returned element.)
2584 - The second element is a JSON schema containing all definitions referenced in the first returned
2585 element, along with the optional title and description keys.
2586 """
2587 for cls, _ in models:
2588 if isinstance(cls.__pydantic_core_schema__, _mock_val_ser.MockCoreSchema):
2589 cls.__pydantic_core_schema__.rebuild()
2591 instance = schema_generator(by_alias=by_alias, ref_template=ref_template, union_format=union_format)
2592 inputs: list[tuple[type[BaseModel] | type[PydanticDataclass], JsonSchemaMode, CoreSchema]] = [
2593 (m, mode, m.__pydantic_core_schema__) for m, mode in models
2594 ]
2595 json_schemas_map, definitions = instance.generate_definitions(inputs)
2597 json_schema: dict[str, Any] = {}
2598 if definitions:
2599 json_schema['$defs'] = definitions
2600 if title:
2601 json_schema['title'] = title
2602 if description:
2603 json_schema['description'] = description
2605 return json_schemas_map, json_schema
2608# ##### End JSON Schema Generation Functions #####
2611_HashableJsonValue: TypeAlias = Union[
2612 int, float, str, bool, None, tuple['_HashableJsonValue', ...], tuple[tuple[str, '_HashableJsonValue'], ...]
2613]
2616def _deduplicate_schemas(schemas: Iterable[JsonDict]) -> list[JsonDict]:
2617 return list({_make_json_hashable(schema): schema for schema in schemas}.values())
2620def _make_json_hashable(value: JsonValue) -> _HashableJsonValue:
2621 if isinstance(value, dict):
2622 return tuple(sorted((k, _make_json_hashable(v)) for k, v in value.items()))
2623 elif isinstance(value, list):
2624 return tuple(_make_json_hashable(v) for v in value)
2625 else:
2626 return value
2629@dataclasses.dataclass(**_internal_dataclass.slots_true)
2630class WithJsonSchema:
2631 """!!! abstract "Usage Documentation"
2632 [`WithJsonSchema` Annotation](../concepts/json_schema.md#withjsonschema-annotation)
2634 Add this as an annotation on a field to override the (base) JSON schema that would be generated for that field.
2635 This provides a way to set a JSON schema for types that would otherwise raise errors when producing a JSON schema,
2636 such as Callable, or types that have an is-instance core schema, without needing to go so far as creating a
2637 custom subclass of pydantic.json_schema.GenerateJsonSchema.
2638 Note that any _modifications_ to the schema that would normally be made (such as setting the title for model fields)
2639 will still be performed.
2641 If `mode` is set this will only apply to that schema generation mode, allowing you
2642 to set different json schemas for validation and serialization.
2643 """
2645 json_schema: JsonSchemaValue | None
2646 mode: Literal['validation', 'serialization'] | None = None
2648 def __get_pydantic_json_schema__(
2649 self, core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler
2650 ) -> JsonSchemaValue:
2651 mode = self.mode or handler.mode
2652 if mode != handler.mode:
2653 return handler(core_schema)
2654 if self.json_schema is None:
2655 # This exception is handled in pydantic.json_schema.GenerateJsonSchema._named_required_fields_schema
2656 raise PydanticOmit
2657 else:
2658 return self.json_schema.copy()
2660 def __hash__(self) -> int:
2661 return hash(type(self.mode))
2664class Examples:
2665 """Add examples to a JSON schema.
2667 If the JSON Schema already contains examples, the provided examples
2668 will be appended.
2670 If `mode` is set this will only apply to that schema generation mode,
2671 allowing you to add different examples for validation and serialization.
2672 """
2674 @overload
2675 @deprecated('Using a dict for `examples` is deprecated since v2.9 and will be removed in v3.0. Use a list instead.')
2676 def __init__(
2677 self, examples: dict[str, Any], mode: Literal['validation', 'serialization'] | None = None
2678 ) -> None: ...
2680 @overload
2681 def __init__(self, examples: list[Any], mode: Literal['validation', 'serialization'] | None = None) -> None: ...
2683 def __init__(
2684 self, examples: dict[str, Any] | list[Any], mode: Literal['validation', 'serialization'] | None = None
2685 ) -> None:
2686 if isinstance(examples, dict):
2687 warnings.warn(
2688 'Using a dict for `examples` is deprecated, use a list instead.',
2689 PydanticDeprecatedSince29,
2690 stacklevel=2,
2691 )
2692 self.examples = examples
2693 self.mode = mode
2695 def __get_pydantic_json_schema__(
2696 self, core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler
2697 ) -> JsonSchemaValue:
2698 mode = self.mode or handler.mode
2699 json_schema = handler(core_schema)
2700 if mode != handler.mode:
2701 return json_schema
2702 examples = json_schema.get('examples')
2703 if examples is None:
2704 json_schema['examples'] = to_jsonable_python(self.examples)
2705 if isinstance(examples, dict):
2706 if isinstance(self.examples, list):
2707 warnings.warn(
2708 'Updating existing JSON Schema examples of type dict with examples of type list. '
2709 'Only the existing examples values will be retained. Note that dict support for '
2710 'examples is deprecated and will be removed in v3.0.',
2711 UserWarning,
2712 )
2713 json_schema['examples'] = to_jsonable_python(
2714 [ex for value in examples.values() for ex in value] + self.examples
2715 )
2716 else:
2717 json_schema['examples'] = to_jsonable_python({**examples, **self.examples})
2718 if isinstance(examples, list):
2719 if isinstance(self.examples, list):
2720 json_schema['examples'] = to_jsonable_python(examples + self.examples)
2721 elif isinstance(self.examples, dict):
2722 warnings.warn(
2723 'Updating existing JSON Schema examples of type list with examples of type dict. '
2724 'Only the examples values will be retained. Note that dict support for '
2725 'examples is deprecated and will be removed in v3.0.',
2726 UserWarning,
2727 )
2728 json_schema['examples'] = to_jsonable_python(
2729 examples + [ex for value in self.examples.values() for ex in value]
2730 )
2732 return json_schema
2734 def __hash__(self) -> int:
2735 return hash(type(self.mode))
2738def _get_all_json_refs(item: Any) -> set[JsonRef]:
2739 """Get all the definitions references from a JSON schema."""
2740 refs: set[JsonRef] = set()
2741 stack = [item]
2743 while stack:
2744 current = stack.pop()
2745 if isinstance(current, dict):
2746 for key, value in current.items():
2747 if key == 'examples' and isinstance(value, list):
2748 # Skip examples that may contain arbitrary values and references
2749 # (e.g. `{"examples": [{"$ref": "..."}]}`). Note: checking for value
2750 # of type list is necessary to avoid skipping valid portions of the schema,
2751 # for instance when "examples" is used as a property key. A more robust solution
2752 # could be found, but would require more advanced JSON Schema parsing logic.
2753 continue
2754 if key == '$ref' and isinstance(value, str):
2755 refs.add(JsonRef(value))
2756 elif isinstance(value, dict):
2757 stack.append(value)
2758 elif isinstance(value, list):
2759 stack.extend(value)
2760 elif isinstance(current, list):
2761 stack.extend(current)
2763 return refs
2766AnyType = TypeVar('AnyType')
2768if TYPE_CHECKING:
2769 SkipJsonSchema = Annotated[AnyType, ...]
2770else:
2772 @dataclasses.dataclass(**_internal_dataclass.slots_true)
2773 class SkipJsonSchema:
2774 """!!! abstract "Usage Documentation"
2775 [`SkipJsonSchema` Annotation](../concepts/json_schema.md#skipjsonschema-annotation)
2777 Add this as an annotation on a field to skip generating a JSON schema for that field.
2779 Example:
2780 ```python
2781 from pprint import pprint
2782 from typing import Union
2784 from pydantic import BaseModel
2785 from pydantic.json_schema import SkipJsonSchema
2787 class Model(BaseModel):
2788 a: Union[int, None] = None # (1)!
2789 b: Union[int, SkipJsonSchema[None]] = None # (2)!
2790 c: SkipJsonSchema[Union[int, None]] = None # (3)!
2792 pprint(Model.model_json_schema())
2793 '''
2794 {
2795 'properties': {
2796 'a': {
2797 'anyOf': [
2798 {'type': 'integer'},
2799 {'type': 'null'}
2800 ],
2801 'default': None,
2802 'title': 'A'
2803 },
2804 'b': {
2805 'default': None,
2806 'title': 'B',
2807 'type': 'integer'
2808 }
2809 },
2810 'title': 'Model',
2811 'type': 'object'
2812 }
2813 '''
2814 ```
2816 1. The integer and null types are both included in the schema for `a`.
2817 2. The integer type is the only type included in the schema for `b`.
2818 3. The entirety of the `c` field is omitted from the schema.
2819 """
2821 def __class_getitem__(cls, item: AnyType) -> AnyType:
2822 return Annotated[item, cls()]
2824 def __get_pydantic_json_schema__(
2825 self, core_schema: CoreSchema, handler: GetJsonSchemaHandler
2826 ) -> JsonSchemaValue:
2827 raise PydanticOmit
2829 def __hash__(self) -> int:
2830 return hash(type(self))
2833def _get_typed_dict_config(cls: type[Any] | None) -> ConfigDict:
2834 if cls is not None:
2835 try:
2836 return _decorators.get_attribute_from_bases(cls, '__pydantic_config__')
2837 except AttributeError:
2838 pass
2839 return {}
2842def _get_ser_schema_for_default_value(schema: CoreSchema) -> core_schema.PlainSerializerFunctionSerSchema | None:
2843 """Get a `'function-plain'` serialization schema that can be used to serialize a default value.
2845 This takes into account having the serialization schema nested under validation schema(s).
2846 """
2847 if (
2848 (ser_schema := schema.get('serialization'))
2849 and ser_schema['type'] == 'function-plain'
2850 and not ser_schema.get('info_arg')
2851 ):
2852 return ser_schema
2853 if _core_utils.is_function_with_inner_schema(schema):
2854 return _get_ser_schema_for_default_value(schema['schema'])