1"""!!! abstract "Usage Documentation"
2 [JSON Schema](../concepts/json_schema.md)
3
4The `json_schema` module contains classes and functions to allow the way [JSON Schema](https://json-schema.org/)
5is generated to be customized.
6
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"""
11
12from __future__ import annotations as _annotations
13
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)
37
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
43
44from pydantic.warnings import PydanticDeprecatedSince26, PydanticDeprecatedSince29
45
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
58
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
65
66
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"""
73
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"""
78
79JsonSchemaMode = Literal['validation', 'serialization']
80"""
81A type alias that represents the mode of a JSON schema; either 'validation' or 'serialization'.
82
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"""
88
89_MODE_TITLE_MAPPING: dict[JsonSchemaMode, str] = {'validation': 'Input', 'serialization': 'Output'}
90
91
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.
95
96See [`GenerateJsonSchema.render_warning_message`][pydantic.json_schema.GenerateJsonSchema.render_warning_message]
97for more details.
98"""
99
100
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 """
107
108
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"""
113
114
115# ##### JSON Schema Generation #####
116DEFAULT_REF_TEMPLATE = '#/$defs/{model}'
117"""The default format string used to generate reference names."""
118
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)
130
131CoreModeRef = tuple[CoreRef, JsonSchemaMode]
132JsonSchemaKeyT = TypeVar('JsonSchemaKeyT', bound=Hashable)
133
134_PRIMITIVE_JSON_SCHEMA_TYPES = ('string', 'boolean', 'null', 'integer', 'number')
135
136
137@dataclasses.dataclass(**_internal_dataclass.slots_true)
138class _DefinitionsRemapping:
139 defs_remapping: dict[DefsRef, DefsRef]
140 json_remapping: dict[JsonRef, JsonRef]
141
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])
165
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])
170
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
186
187 raise PydanticInvalidForJsonSchema('Failed to simplify the JSON schema definitions')
188
189 def remap_defs_ref(self, ref: DefsRef) -> DefsRef:
190 return self.defs_remapping.get(ref, ref)
191
192 def remap_json_ref(self, ref: JsonRef) -> JsonRef:
193 return self.json_remapping.get(ref, ref)
194
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
216
217
218class GenerateJsonSchema:
219 """!!! abstract "Usage Documentation"
220 [Customizing the JSON Schema Generation Process](../concepts/json_schema.md#customizing-the-json-schema-generation-process)
221
222 A class for generating JSON schemas.
223
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.
228
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.
243
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:
248
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`.
255
256 Raises:
257 JsonSchemaError: If the instance of the class is inadvertently reused after generating a schema.
258 """
259
260 schema_dialect = 'https://json-schema.org/draft/2020-12/schema'
261
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'}
265
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
275
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] = {}
280
281 self.definitions: dict[DefsRef, JsonSchemaValue] = {}
282 self._config_wrapper_stack = _config.ConfigWrapperStack(_config.ConfigWrapper({}))
283
284 self._mode: JsonSchemaMode = 'validation'
285
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] = {}
293
294 self._schema_type_to_method = self.build_schema_type_to_method()
295
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] = {}
303
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
307
308 @property
309 def _config(self) -> _config.ConfigWrapper:
310 return self._config_wrapper_stack.tail
311
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
318
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.
323
324 Returns:
325 A dictionary containing the mapping of `CoreSchemaOrFieldType` to a handler method.
326
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
344
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.
350
351 Args:
352 inputs: A sequence of tuples, where:
353
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.
357
358 Returns:
359 A tuple where:
360
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.
366
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 )
376
377 for _, mode, schema in inputs:
378 self._mode = mode
379 self.generate_inner(schema)
380
381 definitions_remapping = self._build_definitions_remapping()
382
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)
388
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
393
394 def generate(self, schema: CoreSchema, mode: JsonSchemaMode = 'validation') -> JsonSchemaValue:
395 """Generates a JSON schema for a specified schema in a specified mode.
396
397 Args:
398 schema: A Pydantic model.
399 mode: The mode in which to generate the schema. Defaults to 'validation'.
400
401 Returns:
402 A JSON schema representing the specified schema.
403
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 )
414
415 json_schema: JsonSchemaValue = self.generate_inner(schema)
416 json_ref_counts = self.get_json_ref_counts(json_schema)
417
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
427
428 self._garbage_collect_definitions(json_schema)
429 definitions_remapping = self._build_definitions_remapping()
430
431 if self.definitions:
432 json_schema['$defs'] = self.definitions
433
434 json_schema = definitions_remapping.remap_json_schema(json_schema)
435
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
439
440 self._used = True
441 return self.sort(json_schema)
442
443 def generate_inner(self, schema: CoreSchemaOrField) -> JsonSchemaValue: # noqa: C901
444 """Generates a JSON schema for a given core schema.
445
446 Args:
447 schema: The given core schema.
448
449 Returns:
450 The generated JSON schema.
451
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]}
464
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
478
479 def handler_func(schema_or_field: CoreSchemaOrField) -> JsonSchemaValue:
480 """Generate a JSON schema based on the input schema.
481
482 Args:
483 schema_or_field: The core schema to generate a JSON schema from.
484
485 Returns:
486 The generated JSON schema.
487
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)
499
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
515
516 current_handler = _schema_generation_shared.GenerateJsonSchemaHandler(self, handler_func)
517
518 metadata = cast(_core_metadata.CoreMetadata, schema.get('metadata', {}))
519
520 # TODO: I dislike that we have to wrap these basic dict updates in callables, is there any way around this?
521
522 if js_updates := metadata.get('pydantic_js_updates'):
523
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
530
531 current_handler = _schema_generation_shared.GenerateJsonSchemaHandler(self, js_updates_handler_func)
532
533 if js_extra := metadata.get('pydantic_js_extra'):
534
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
546
547 current_handler = _schema_generation_shared.GenerateJsonSchemaHandler(self, js_extra_handler_func)
548
549 for js_modify_function in metadata.get('pydantic_js_functions', ()):
550
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
564
565 current_handler = _schema_generation_shared.GenerateJsonSchemaHandler(self, new_handler_func)
566
567 for js_modify_function in metadata.get('pydantic_js_annotation_functions', ()):
568
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)
575
576 current_handler = _schema_generation_shared.GenerateJsonSchemaHandler(self, new_handler_func)
577
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
582
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.)
585
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
596
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
612
613 # ### Schema generation methods
614
615 def invalid_schema(self, schema: core_schema.InvalidSchema) -> JsonSchemaValue:
616 """Placeholder - should never be called."""
617
618 raise RuntimeError('Cannot generate schema for invalid_schema. This is a bug! Please report it.')
619
620 def any_schema(self, schema: core_schema.AnySchema) -> JsonSchemaValue:
621 """Generates a JSON schema that matches any value.
622
623 Args:
624 schema: The core schema.
625
626 Returns:
627 The generated JSON schema.
628 """
629 return {}
630
631 def none_schema(self, schema: core_schema.NoneSchema) -> JsonSchemaValue:
632 """Generates a JSON schema that matches `None`.
633
634 Args:
635 schema: The core schema.
636
637 Returns:
638 The generated JSON schema.
639 """
640 return {'type': 'null'}
641
642 def bool_schema(self, schema: core_schema.BoolSchema) -> JsonSchemaValue:
643 """Generates a JSON schema that matches a bool value.
644
645 Args:
646 schema: The core schema.
647
648 Returns:
649 The generated JSON schema.
650 """
651 return {'type': 'boolean'}
652
653 def int_schema(self, schema: core_schema.IntSchema) -> JsonSchemaValue:
654 """Generates a JSON schema that matches an int value.
655
656 Args:
657 schema: The core schema.
658
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
666
667 def float_schema(self, schema: core_schema.FloatSchema) -> JsonSchemaValue:
668 """Generates a JSON schema that matches a float value.
669
670 Args:
671 schema: The core schema.
672
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
680
681 def decimal_schema(self, schema: core_schema.DecimalSchema) -> JsonSchemaValue:
682 """Generates a JSON schema that matches a decimal value.
683
684 Args:
685 schema: The core schema.
686
687 Returns:
688 The generated JSON schema.
689 """
690
691 def get_decimal_pattern(schema: core_schema.DecimalSchema) -> str:
692 max_digits = schema.get('max_digits')
693 decimal_places = schema.get('decimal_places')
694
695 pattern = (
696 r'^(?!^[-+.]*$)[+-]?0*' # check it is not empty string and not one or sequence of ".+-" characters.
697 )
698
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 )
710
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 )
721
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*$'
725
726 # Case 4: Both are None (no restrictions)
727 else:
728 pattern += r'\d*\.?\d*$' # look for arbitrary integer or decimal
729
730 return pattern
731
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
755
756 def str_schema(self, schema: core_schema.StringSchema) -> JsonSchemaValue:
757 """Generates a JSON schema that matches a string value.
758
759 Args:
760 schema: The core schema.
761
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
771
772 def bytes_schema(self, schema: core_schema.BytesSchema) -> JsonSchemaValue:
773 """Generates a JSON schema that matches a bytes value.
774
775 Args:
776 schema: The core schema.
777
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
784
785 def date_schema(self, schema: core_schema.DateSchema) -> JsonSchemaValue:
786 """Generates a JSON schema that matches a date value.
787
788 Args:
789 schema: The core schema.
790
791 Returns:
792 The generated JSON schema.
793 """
794 return {'type': 'string', 'format': 'date'}
795
796 def time_schema(self, schema: core_schema.TimeSchema) -> JsonSchemaValue:
797 """Generates a JSON schema that matches a time value.
798
799 Args:
800 schema: The core schema.
801
802 Returns:
803 The generated JSON schema.
804 """
805 return {'type': 'string', 'format': 'time'}
806
807 def datetime_schema(self, schema: core_schema.DatetimeSchema) -> JsonSchemaValue:
808 """Generates a JSON schema that matches a datetime value.
809
810 Args:
811 schema: The core schema.
812
813 Returns:
814 The generated JSON schema.
815 """
816 return {'type': 'string', 'format': 'date-time'}
817
818 def timedelta_schema(self, schema: core_schema.TimedeltaSchema) -> JsonSchemaValue:
819 """Generates a JSON schema that matches a timedelta value.
820
821 Args:
822 schema: The core schema.
823
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'}
830
831 def literal_schema(self, schema: core_schema.LiteralSchema) -> JsonSchemaValue:
832 """Generates a JSON schema that matches a literal value.
833
834 Args:
835 schema: The core schema.
836
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']]
841
842 result: dict[str, Any] = {}
843 if len(expected) == 1:
844 result['const'] = expected[0]
845 else:
846 result['enum'] = expected
847
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
862
863 def missing_sentinel_schema(self, schema: core_schema.MissingSentinelSchema) -> JsonSchemaValue:
864 """Generates a JSON schema that matches the `MISSING` sentinel value.
865
866 Args:
867 schema: The core schema.
868
869 Returns:
870 The generated JSON schema.
871 """
872 raise PydanticOmit
873
874 def enum_schema(self, schema: core_schema.EnumSchema) -> JsonSchemaValue:
875 """Generates a JSON schema that matches an Enum value.
876
877 Args:
878 schema: The core schema.
879
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}
891
892 expected = [to_jsonable_python(v.value) for v in schema['members']]
893
894 result['enum'] = expected
895
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'
907
908 return result
909
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.
912
913 Unless overridden in a subclass, this raises an error.
914
915 Args:
916 schema: The core schema.
917
918 Returns:
919 The generated JSON schema.
920 """
921 return self.handle_invalid_for_json_schema(schema, f'core_schema.IsInstanceSchema ({schema["cls"]})')
922
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.
925
926 For backwards compatibility with v1, this does not raise an error, but can be overridden to change this.
927
928 Args:
929 schema: The core schema.
930
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 {}
936
937 def callable_schema(self, schema: core_schema.CallableSchema) -> JsonSchemaValue:
938 """Generates a JSON schema that matches a callable value.
939
940 Unless overridden in a subclass, this raises an error.
941
942 Args:
943 schema: The core schema.
944
945 Returns:
946 The generated JSON schema.
947 """
948 return self.handle_invalid_for_json_schema(schema, 'core_schema.CallableSchema')
949
950 def list_schema(self, schema: core_schema.ListSchema) -> JsonSchemaValue:
951 """Returns a schema that matches a list schema.
952
953 Args:
954 schema: The core schema.
955
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
963
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)
974
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)
985
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, ...]`.
989
990 Args:
991 schema: The core schema.
992
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
1020
1021 def set_schema(self, schema: core_schema.SetSchema) -> JsonSchemaValue:
1022 """Generates a JSON schema that matches a set schema.
1023
1024 Args:
1025 schema: The core schema.
1026
1027 Returns:
1028 The generated JSON schema.
1029 """
1030 return self._common_set_schema(schema)
1031
1032 def frozenset_schema(self, schema: core_schema.FrozenSetSchema) -> JsonSchemaValue:
1033 """Generates a JSON schema that matches a frozenset schema.
1034
1035 Args:
1036 schema: The core schema.
1037
1038 Returns:
1039 The generated JSON schema.
1040 """
1041 return self._common_set_schema(schema)
1042
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
1048
1049 def generator_schema(self, schema: core_schema.GeneratorSchema) -> JsonSchemaValue:
1050 """Returns a JSON schema that represents the provided GeneratorSchema.
1051
1052 Args:
1053 schema: The schema.
1054
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
1062
1063 def dict_schema(self, schema: core_schema.DictSchema) -> JsonSchemaValue:
1064 """Generates a JSON schema that matches a dict schema.
1065
1066 Args:
1067 schema: The core schema.
1068
1069 Returns:
1070 The generated JSON schema.
1071 """
1072 json_schema: JsonSchemaValue = {'type': 'object'}
1073
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
1086
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)
1090
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
1098
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
1107
1108 self.update_with_validations(json_schema, schema, self.ValidationsMapping.object)
1109 return json_schema
1110
1111 def function_before_schema(self, schema: core_schema.BeforeValidatorFunctionSchema) -> JsonSchemaValue:
1112 """Generates a JSON schema that matches a function-before schema.
1113
1114 Args:
1115 schema: The core schema.
1116
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)
1122
1123 return self.generate_inner(schema['schema'])
1124
1125 def function_after_schema(self, schema: core_schema.AfterValidatorFunctionSchema) -> JsonSchemaValue:
1126 """Generates a JSON schema that matches a function-after schema.
1127
1128 Args:
1129 schema: The core schema.
1130
1131 Returns:
1132 The generated JSON schema.
1133 """
1134 return self.generate_inner(schema['schema'])
1135
1136 def function_plain_schema(self, schema: core_schema.PlainValidatorFunctionSchema) -> JsonSchemaValue:
1137 """Generates a JSON schema that matches a function-plain schema.
1138
1139 Args:
1140 schema: The core schema.
1141
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)
1147
1148 return self.handle_invalid_for_json_schema(
1149 schema, f'core_schema.PlainValidatorFunctionSchema ({schema["function"]})'
1150 )
1151
1152 def function_wrap_schema(self, schema: core_schema.WrapValidatorFunctionSchema) -> JsonSchemaValue:
1153 """Generates a JSON schema that matches a function-wrap schema.
1154
1155 Args:
1156 schema: The core schema.
1157
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)
1163
1164 return self.generate_inner(schema['schema'])
1165
1166 def default_schema(self, schema: core_schema.WithDefaultSchema) -> JsonSchemaValue:
1167 """Generates a JSON schema that matches a schema with a default value.
1168
1169 Args:
1170 schema: The core schema.
1171
1172 Returns:
1173 The generated JSON schema.
1174 """
1175 json_schema = self.generate_inner(schema['schema'])
1176
1177 default = self.get_default_value(schema)
1178 if default is NoDefault or default is MISSING:
1179 return json_schema
1180
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
1206
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
1216
1217 json_schema['default'] = encoded_default
1218 return json_schema
1219
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.
1222
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.
1225
1226 Args:
1227 schema: The `'with-default'` core schema.
1228
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)
1234
1235 def nullable_schema(self, schema: core_schema.NullableSchema) -> JsonSchemaValue:
1236 """Generates a JSON schema that matches a schema that allows null values.
1237
1238 Args:
1239 schema: The core schema.
1240
1241 Returns:
1242 The generated JSON schema.
1243 """
1244 null_schema = {'type': 'null'}
1245 inner_json_schema = self.generate_inner(schema['schema'])
1246
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])
1251
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.
1254
1255 Args:
1256 schema: The core schema.
1257
1258 Returns:
1259 The generated JSON schema.
1260 """
1261 generated: list[JsonSchemaValue] = []
1262
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)
1276
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.
1279
1280 The result depends on the configured `'union_format'`.
1281
1282 Args:
1283 schemas: The list of JSON Schemas to be included in the union.
1284
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
1305
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))}
1310
1311 return self.get_flattened_anyof(schemas)
1312
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.
1317
1318 Args:
1319 schema: The core schema.
1320
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)
1336
1337 one_of_choices = _deduplicate_schemas(generated.values())
1338 json_schema: JsonSchemaValue = {'oneOf': one_of_choices}
1339
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 }
1347
1348 return json_schema
1349
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
1356
1357 if isinstance(schema['discriminator'], str):
1358 return schema['discriminator']
1359
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
1394
1395 def chain_schema(self, schema: core_schema.ChainSchema) -> JsonSchemaValue:
1396 """Generates a JSON schema that matches a core_schema.ChainSchema.
1397
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.
1400
1401 Args:
1402 schema: The core schema.
1403
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])
1409
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.
1413
1414 Args:
1415 schema: The core schema.
1416
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'])
1428
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.
1432
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.
1435
1436 Args:
1437 schema: The core schema.
1438
1439 Returns:
1440 The generated JSON schema.
1441 """
1442 return self.generate_inner(schema['json_schema'])
1443
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.
1446
1447 Args:
1448 schema: The core schema.
1449
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)
1465
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
1479
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
1490
1491 return json_schema
1492
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]
1498
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)
1518
1519 json_schema = {'type': 'object', 'properties': properties}
1520 if required_fields:
1521 json_schema['required'] = required_fields
1522 return json_schema
1523
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
1544
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.
1547
1548 Args:
1549 schema: The core schema.
1550
1551 Returns:
1552 The generated JSON schema.
1553 """
1554 return self.generate_inner(schema['schema'])
1555
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.
1558
1559 Args:
1560 schema: The core schema.
1561
1562 Returns:
1563 The generated JSON schema.
1564 """
1565 return self.generate_inner(schema['schema'])
1566
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.
1569
1570 Args:
1571 schema: The core schema.
1572
1573 Returns:
1574 The generated JSON schema.
1575 """
1576 return self.generate_inner(schema['schema'])
1577
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.
1580
1581 Args:
1582 schema: The core schema.
1583
1584 Returns:
1585 The generated JSON schema.
1586 """
1587 return self.generate_inner(schema['return_schema'])
1588
1589 def model_schema(self, schema: core_schema.ModelSchema) -> JsonSchemaValue:
1590 """Generates a JSON schema that matches a schema that defines a model.
1591
1592 Args:
1593 schema: The core schema.
1594
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
1602
1603 with self._config_wrapper_stack.push(config):
1604 json_schema = self.generate_inner(schema['schema'])
1605
1606 self._update_class_schema(json_schema, cls, config)
1607
1608 return json_schema
1609
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`:
1612
1613 * title
1614 * description
1615 * additional properties
1616 * json_schema_extra
1617 * deprecated
1618
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
1624
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__
1634
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__
1637
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)
1642
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
1649
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
1660
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)
1664
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 )
1677
1678 if hasattr(cls, '__deprecated__'):
1679 json_schema['deprecated'] = True
1680
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.
1683
1684 Args:
1685 json_schema: The schema to resolve.
1686
1687 Returns:
1688 The resolved schema.
1689
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
1700
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.
1703
1704 Args:
1705 schema: The core schema.
1706
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
1723
1724 def field_is_present(self, field: CoreSchemaField) -> bool:
1725 """Whether the field should be included in the generated JSON schema.
1726
1727 Args:
1728 field: The schema for the field itself.
1729
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)
1741
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.).
1749
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.
1755
1756 Returns:
1757 `True` if the field should be marked as required in the generated JSON schema, `False` otherwise.
1758 """
1759 if self.mode == 'serialization' and self._config.json_schema_serialization_defaults_required:
1760 return not field.get('serialization_exclude')
1761 else:
1762 if field['type'] == 'typed-dict-field':
1763 return field.get('required', total)
1764 else:
1765 return field['schema']['type'] != 'default'
1766
1767 def dataclass_args_schema(self, schema: core_schema.DataclassArgsSchema) -> JsonSchemaValue:
1768 """Generates a JSON schema that matches a schema that defines a dataclass's constructor arguments.
1769
1770 Args:
1771 schema: The core schema.
1772
1773 Returns:
1774 The generated JSON schema.
1775 """
1776 named_required_fields: list[tuple[str, bool, CoreSchemaField]] = [
1777 (field['name'], self.field_is_required(field, total=True), field)
1778 for field in schema['fields']
1779 if self.field_is_present(field)
1780 ]
1781 if self.mode == 'serialization':
1782 named_required_fields.extend(self._name_required_computed_fields(schema.get('computed_fields', [])))
1783 return self._named_required_fields_schema(named_required_fields)
1784
1785 def dataclass_schema(self, schema: core_schema.DataclassSchema) -> JsonSchemaValue:
1786 """Generates a JSON schema that matches a schema that defines a dataclass.
1787
1788 Args:
1789 schema: The core schema.
1790
1791 Returns:
1792 The generated JSON schema.
1793 """
1794 from ._internal._dataclasses import is_stdlib_dataclass
1795
1796 cls = schema['cls']
1797 config: ConfigDict = getattr(cls, '__pydantic_config__', cast('ConfigDict', {}))
1798
1799 with self._config_wrapper_stack.push(config):
1800 json_schema = self.generate_inner(schema['schema']).copy()
1801
1802 self._update_class_schema(json_schema, cls, config)
1803
1804 # Dataclass-specific handling of description
1805 if is_stdlib_dataclass(cls):
1806 # vanilla dataclass; don't use cls.__doc__ as it will contain the class signature by default
1807 description = None
1808 else:
1809 description = None if cls.__doc__ is None else inspect.cleandoc(cls.__doc__)
1810 if description:
1811 json_schema['description'] = description
1812
1813 return json_schema
1814
1815 def arguments_schema(self, schema: core_schema.ArgumentsSchema) -> JsonSchemaValue:
1816 """Generates a JSON schema that matches a schema that defines a function's arguments.
1817
1818 Args:
1819 schema: The core schema.
1820
1821 Returns:
1822 The generated JSON schema.
1823 """
1824 prefer_positional = schema.get('metadata', {}).get('pydantic_js_prefer_positional_arguments')
1825
1826 arguments = schema['arguments_schema']
1827 kw_only_arguments = [a for a in arguments if a.get('mode') == 'keyword_only']
1828 kw_or_p_arguments = [a for a in arguments if a.get('mode') in {'positional_or_keyword', None}]
1829 p_only_arguments = [a for a in arguments if a.get('mode') == 'positional_only']
1830 var_args_schema = schema.get('var_args_schema')
1831 var_kwargs_schema = schema.get('var_kwargs_schema')
1832
1833 if prefer_positional:
1834 positional_possible = not kw_only_arguments and not var_kwargs_schema
1835 if positional_possible:
1836 return self.p_arguments_schema(p_only_arguments + kw_or_p_arguments, var_args_schema)
1837
1838 keyword_possible = not p_only_arguments and not var_args_schema
1839 if keyword_possible:
1840 return self.kw_arguments_schema(kw_or_p_arguments + kw_only_arguments, var_kwargs_schema)
1841
1842 if not prefer_positional:
1843 positional_possible = not kw_only_arguments and not var_kwargs_schema
1844 if positional_possible:
1845 return self.p_arguments_schema(p_only_arguments + kw_or_p_arguments, var_args_schema)
1846
1847 raise PydanticInvalidForJsonSchema(
1848 'Unable to generate JSON schema for arguments validator with positional-only and keyword-only arguments'
1849 )
1850
1851 def kw_arguments_schema(
1852 self, arguments: list[core_schema.ArgumentsParameter], var_kwargs_schema: CoreSchema | None
1853 ) -> JsonSchemaValue:
1854 """Generates a JSON schema that matches a schema that defines a function's keyword arguments.
1855
1856 Args:
1857 arguments: The core schema.
1858
1859 Returns:
1860 The generated JSON schema.
1861 """
1862 properties: dict[str, JsonSchemaValue] = {}
1863 required: list[str] = []
1864 for argument in arguments:
1865 name = self.get_argument_name(argument)
1866 argument_schema = self.generate_inner(argument['schema']).copy()
1867 if 'title' not in argument_schema and self.field_title_should_be_set(argument['schema']):
1868 argument_schema['title'] = self.get_title_from_name(name)
1869 properties[name] = argument_schema
1870
1871 if argument['schema']['type'] != 'default':
1872 # This assumes that if the argument has a default value,
1873 # the inner schema must be of type WithDefaultSchema.
1874 # I believe this is true, but I am not 100% sure
1875 required.append(name)
1876
1877 json_schema: JsonSchemaValue = {'type': 'object', 'properties': properties}
1878 if required:
1879 json_schema['required'] = required
1880
1881 if var_kwargs_schema:
1882 additional_properties_schema = self.generate_inner(var_kwargs_schema)
1883 if additional_properties_schema:
1884 json_schema['additionalProperties'] = additional_properties_schema
1885 else:
1886 json_schema['additionalProperties'] = False
1887 return json_schema
1888
1889 def p_arguments_schema(
1890 self, arguments: list[core_schema.ArgumentsParameter], var_args_schema: CoreSchema | None
1891 ) -> JsonSchemaValue:
1892 """Generates a JSON schema that matches a schema that defines a function's positional arguments.
1893
1894 Args:
1895 arguments: The core schema.
1896
1897 Returns:
1898 The generated JSON schema.
1899 """
1900 prefix_items: list[JsonSchemaValue] = []
1901 min_items = 0
1902
1903 for argument in arguments:
1904 name = self.get_argument_name(argument)
1905
1906 argument_schema = self.generate_inner(argument['schema']).copy()
1907 if 'title' not in argument_schema and self.field_title_should_be_set(argument['schema']):
1908 argument_schema['title'] = self.get_title_from_name(name)
1909 prefix_items.append(argument_schema)
1910
1911 if argument['schema']['type'] != 'default':
1912 # This assumes that if the argument has a default value,
1913 # the inner schema must be of type WithDefaultSchema.
1914 # I believe this is true, but I am not 100% sure
1915 min_items += 1
1916
1917 json_schema: JsonSchemaValue = {'type': 'array'}
1918 if prefix_items:
1919 json_schema['prefixItems'] = prefix_items
1920 if min_items:
1921 json_schema['minItems'] = min_items
1922
1923 if var_args_schema:
1924 items_schema = self.generate_inner(var_args_schema)
1925 if items_schema:
1926 json_schema['items'] = items_schema
1927 else:
1928 json_schema['maxItems'] = len(prefix_items)
1929
1930 return json_schema
1931
1932 def get_argument_name(self, argument: core_schema.ArgumentsParameter | core_schema.ArgumentsV3Parameter) -> str:
1933 """Retrieves the name of an argument.
1934
1935 Args:
1936 argument: The core schema.
1937
1938 Returns:
1939 The name of the argument.
1940 """
1941 name = argument['name']
1942 if self.by_alias:
1943 alias = argument.get('alias')
1944 if isinstance(alias, str):
1945 name = alias
1946 else:
1947 pass # might want to do something else?
1948 return name
1949
1950 def arguments_v3_schema(self, schema: core_schema.ArgumentsV3Schema) -> JsonSchemaValue:
1951 """Generates a JSON schema that matches a schema that defines a function's arguments.
1952
1953 Args:
1954 schema: The core schema.
1955
1956 Returns:
1957 The generated JSON schema.
1958 """
1959 arguments = schema['arguments_schema']
1960 properties: dict[str, JsonSchemaValue] = {}
1961 required: list[str] = []
1962 for argument in arguments:
1963 mode = argument.get('mode', 'positional_or_keyword')
1964 name = self.get_argument_name(argument)
1965 argument_schema = self.generate_inner(argument['schema']).copy()
1966 if mode == 'var_args':
1967 argument_schema = {'type': 'array', 'items': argument_schema}
1968 elif mode == 'var_kwargs_uniform':
1969 argument_schema = {'type': 'object', 'additionalProperties': argument_schema}
1970
1971 argument_schema.setdefault('title', self.get_title_from_name(name))
1972 properties[name] = argument_schema
1973
1974 if (
1975 (mode == 'var_kwargs_unpacked_typed_dict' and 'required' in argument_schema)
1976 or mode not in {'var_args', 'var_kwargs_uniform', 'var_kwargs_unpacked_typed_dict'}
1977 and argument['schema']['type'] != 'default'
1978 ):
1979 # This assumes that if the argument has a default value,
1980 # the inner schema must be of type WithDefaultSchema.
1981 # I believe this is true, but I am not 100% sure
1982 required.append(name)
1983
1984 json_schema: JsonSchemaValue = {'type': 'object', 'properties': properties}
1985 if required:
1986 json_schema['required'] = required
1987 return json_schema
1988
1989 def call_schema(self, schema: core_schema.CallSchema) -> JsonSchemaValue:
1990 """Generates a JSON schema that matches a schema that defines a function call.
1991
1992 Args:
1993 schema: The core schema.
1994
1995 Returns:
1996 The generated JSON schema.
1997 """
1998 return self.generate_inner(schema['arguments_schema'])
1999
2000 def custom_error_schema(self, schema: core_schema.CustomErrorSchema) -> JsonSchemaValue:
2001 """Generates a JSON schema that matches a schema that defines a custom error.
2002
2003 Args:
2004 schema: The core schema.
2005
2006 Returns:
2007 The generated JSON schema.
2008 """
2009 return self.generate_inner(schema['schema'])
2010
2011 def json_schema(self, schema: core_schema.JsonSchema) -> JsonSchemaValue:
2012 """Generates a JSON schema that matches a schema that defines a JSON object.
2013
2014 Args:
2015 schema: The core schema.
2016
2017 Returns:
2018 The generated JSON schema.
2019 """
2020 content_core_schema = schema.get('schema') or core_schema.any_schema()
2021 content_json_schema = self.generate_inner(content_core_schema)
2022 if self.mode == 'validation':
2023 return {'type': 'string', 'contentMediaType': 'application/json', 'contentSchema': content_json_schema}
2024 else:
2025 # self.mode == 'serialization'
2026 return content_json_schema
2027
2028 def url_schema(self, schema: core_schema.UrlSchema) -> JsonSchemaValue:
2029 """Generates a JSON schema that matches a schema that defines a URL.
2030
2031 Args:
2032 schema: The core schema.
2033
2034 Returns:
2035 The generated JSON schema.
2036 """
2037 json_schema = {'type': 'string', 'format': 'uri', 'minLength': 1}
2038 self.update_with_validations(json_schema, schema, self.ValidationsMapping.string)
2039 return json_schema
2040
2041 def multi_host_url_schema(self, schema: core_schema.MultiHostUrlSchema) -> JsonSchemaValue:
2042 """Generates a JSON schema that matches a schema that defines a URL that can be used with multiple hosts.
2043
2044 Args:
2045 schema: The core schema.
2046
2047 Returns:
2048 The generated JSON schema.
2049 """
2050 # Note: 'multi-host-uri' is a custom/pydantic-specific format, not part of the JSON Schema spec
2051 json_schema = {'type': 'string', 'format': 'multi-host-uri', 'minLength': 1}
2052 self.update_with_validations(json_schema, schema, self.ValidationsMapping.string)
2053 return json_schema
2054
2055 def uuid_schema(self, schema: core_schema.UuidSchema) -> JsonSchemaValue:
2056 """Generates a JSON schema that matches a UUID.
2057
2058 Args:
2059 schema: The core schema.
2060
2061 Returns:
2062 The generated JSON schema.
2063 """
2064 return {'type': 'string', 'format': 'uuid'}
2065
2066 def definitions_schema(self, schema: core_schema.DefinitionsSchema) -> JsonSchemaValue:
2067 """Generates a JSON schema that matches a schema that defines a JSON object with definitions.
2068
2069 Args:
2070 schema: The core schema.
2071
2072 Returns:
2073 The generated JSON schema.
2074 """
2075 for definition in schema['definitions']:
2076 try:
2077 self.generate_inner(definition)
2078 except PydanticInvalidForJsonSchema as e: # noqa: PERF203
2079 core_ref: CoreRef = CoreRef(definition['ref']) # type: ignore
2080 self._core_defs_invalid_for_json_schema[self.get_defs_ref((core_ref, self.mode))] = e
2081 continue
2082 return self.generate_inner(schema['schema'])
2083
2084 def definition_ref_schema(self, schema: core_schema.DefinitionReferenceSchema) -> JsonSchemaValue:
2085 """Generates a JSON schema that matches a schema that references a definition.
2086
2087 Args:
2088 schema: The core schema.
2089
2090 Returns:
2091 The generated JSON schema.
2092 """
2093 core_ref = CoreRef(schema['schema_ref'])
2094 _, ref_json_schema = self.get_cache_defs_ref_schema(core_ref)
2095 return ref_json_schema
2096
2097 def ser_schema(
2098 self, schema: core_schema.SerSchema | core_schema.IncExSeqSerSchema | core_schema.IncExDictSerSchema
2099 ) -> JsonSchemaValue | None:
2100 """Generates a JSON schema that matches a schema that defines a serialized object.
2101
2102 Args:
2103 schema: The core schema.
2104
2105 Returns:
2106 The generated JSON schema.
2107 """
2108 schema_type = schema['type']
2109 if schema_type == 'function-plain' or schema_type == 'function-wrap':
2110 # PlainSerializerFunctionSerSchema or WrapSerializerFunctionSerSchema
2111 return_schema = schema.get('return_schema')
2112 if return_schema is not None:
2113 return self.generate_inner(return_schema)
2114 elif schema_type == 'format' or schema_type == 'to-string':
2115 # FormatSerSchema or ToStringSerSchema
2116 return self.str_schema(core_schema.str_schema())
2117 elif schema['type'] == 'model':
2118 # ModelSerSchema
2119 return self.generate_inner(schema['schema'])
2120 return None
2121
2122 def complex_schema(self, schema: core_schema.ComplexSchema) -> JsonSchemaValue:
2123 """Generates a JSON schema that matches a complex number.
2124
2125 JSON has no standard way to represent complex numbers. Complex number is not a numeric
2126 type. Here we represent complex number as strings following the rule defined by Python.
2127 For instance, '1+2j' is an accepted complex string. Details can be found in
2128 [Python's `complex` documentation][complex].
2129
2130 Args:
2131 schema: The core schema.
2132
2133 Returns:
2134 The generated JSON schema.
2135 """
2136 return {'type': 'string'}
2137
2138 # ### Utility methods
2139
2140 def get_title_from_name(self, name: str) -> str:
2141 """Retrieves a title from a name.
2142
2143 Args:
2144 name: The name to retrieve a title from.
2145
2146 Returns:
2147 The title.
2148 """
2149 return name.title().replace('_', ' ').strip()
2150
2151 def field_title_should_be_set(self, schema: CoreSchemaOrField) -> bool:
2152 """Returns true if a field with the given schema should have a title set based on the field name.
2153
2154 Intuitively, we want this to return true for schemas that wouldn't otherwise provide their own title
2155 (e.g., int, float, str), and false for those that would (e.g., BaseModel subclasses).
2156
2157 Args:
2158 schema: The schema to check.
2159
2160 Returns:
2161 `True` if the field should have a title set, `False` otherwise.
2162 """
2163 if _core_utils.is_core_schema_field(schema):
2164 if schema['type'] == 'computed-field':
2165 field_schema = schema['return_schema']
2166 else:
2167 field_schema = schema['schema']
2168 return self.field_title_should_be_set(field_schema)
2169
2170 elif _core_utils.is_core_schema(schema):
2171 if schema.get('ref'): # things with refs, such as models and enums, should not have titles set
2172 return False
2173 if schema['type'] in {'default', 'nullable', 'definitions'}:
2174 return self.field_title_should_be_set(schema['schema']) # type: ignore[typeddict-item]
2175 if _core_utils.is_function_with_inner_schema(schema):
2176 return self.field_title_should_be_set(schema['schema'])
2177 if schema['type'] == 'definition-ref':
2178 # Referenced schemas should not have titles set for the same reason
2179 # schemas with refs should not
2180 return False
2181 return True # anything else should have title set
2182
2183 else:
2184 raise PydanticInvalidForJsonSchema(f'Unexpected schema type: schema={schema}') # pragma: no cover
2185
2186 def normalize_name(self, name: str) -> str:
2187 """Normalizes a name to be used as a key in a dictionary.
2188
2189 Args:
2190 name: The name to normalize.
2191
2192 Returns:
2193 The normalized name.
2194 """
2195 return re.sub(r'[^a-zA-Z0-9.\-_]', '_', name).replace('.', '__')
2196
2197 def get_defs_ref(self, core_mode_ref: CoreModeRef) -> DefsRef:
2198 """Override this method to change the way that definitions keys are generated from a core reference.
2199
2200 Args:
2201 core_mode_ref: The core reference.
2202
2203 Returns:
2204 The definitions key.
2205 """
2206 # Split the core ref into "components"; generic origins and arguments are each separate components
2207 core_ref, mode = core_mode_ref
2208 components = re.split(r'([\][,])', core_ref)
2209 # Remove IDs from each component
2210 components = [x.rsplit(':', 1)[0] for x in components]
2211 core_ref_no_id = ''.join(components)
2212 # Remove everything before the last period from each "component"
2213 components = [re.sub(r'(?:[^.[\]]+\.)+((?:[^.[\]]+))', r'\1', x) for x in components]
2214 short_ref = ''.join(components)
2215
2216 mode_title = _MODE_TITLE_MAPPING[mode]
2217
2218 # It is important that the generated defs_ref values be such that at least one choice will not
2219 # be generated for any other core_ref. Currently, this should be the case because we include
2220 # the id of the source type in the core_ref
2221 name = DefsRef(self.normalize_name(short_ref))
2222 name_mode = DefsRef(self.normalize_name(short_ref) + f'-{mode_title}')
2223 module_qualname = DefsRef(self.normalize_name(core_ref_no_id))
2224 module_qualname_mode = DefsRef(f'{module_qualname}-{mode_title}')
2225 module_qualname_id = DefsRef(self.normalize_name(core_ref))
2226 occurrence_index = self._collision_index.get(module_qualname_id)
2227 if occurrence_index is None:
2228 self._collision_counter[module_qualname] += 1
2229 occurrence_index = self._collision_index[module_qualname_id] = self._collision_counter[module_qualname]
2230
2231 module_qualname_occurrence = DefsRef(f'{module_qualname}__{occurrence_index}')
2232 module_qualname_occurrence_mode = DefsRef(f'{module_qualname_mode}__{occurrence_index}')
2233
2234 self._prioritized_defsref_choices[module_qualname_occurrence_mode] = [
2235 name,
2236 name_mode,
2237 module_qualname,
2238 module_qualname_mode,
2239 module_qualname_occurrence,
2240 module_qualname_occurrence_mode,
2241 ]
2242
2243 return module_qualname_occurrence_mode
2244
2245 def get_cache_defs_ref_schema(self, core_ref: CoreRef) -> tuple[DefsRef, JsonSchemaValue]:
2246 """This method wraps the get_defs_ref method with some cache-lookup/population logic,
2247 and returns both the produced defs_ref and the JSON schema that will refer to the right definition.
2248
2249 Args:
2250 core_ref: The core reference to get the definitions reference for.
2251
2252 Returns:
2253 A tuple of the definitions reference and the JSON schema that will refer to it.
2254 """
2255 core_mode_ref = (core_ref, self.mode)
2256 maybe_defs_ref = self.core_to_defs_refs.get(core_mode_ref)
2257 if maybe_defs_ref is not None:
2258 json_ref = self.core_to_json_refs[core_mode_ref]
2259 return maybe_defs_ref, {'$ref': json_ref}
2260
2261 defs_ref = self.get_defs_ref(core_mode_ref)
2262
2263 # populate the ref translation mappings
2264 self.core_to_defs_refs[core_mode_ref] = defs_ref
2265 self.defs_to_core_refs[defs_ref] = core_mode_ref
2266
2267 json_ref = JsonRef(self.ref_template.format(model=defs_ref))
2268 self.core_to_json_refs[core_mode_ref] = json_ref
2269 self.json_to_defs_refs[json_ref] = defs_ref
2270 ref_json_schema = {'$ref': json_ref}
2271 return defs_ref, ref_json_schema
2272
2273 def handle_ref_overrides(self, json_schema: JsonSchemaValue) -> JsonSchemaValue:
2274 """Remove any sibling keys that are redundant with the referenced schema.
2275
2276 Args:
2277 json_schema: The schema to remove redundant sibling keys from.
2278
2279 Returns:
2280 The schema with redundant sibling keys removed.
2281 """
2282 if '$ref' in json_schema:
2283 # prevent modifications to the input; this copy may be safe to drop if there is significant overhead
2284 json_schema = json_schema.copy()
2285
2286 referenced_json_schema = self.get_schema_from_definitions(JsonRef(json_schema['$ref']))
2287 if referenced_json_schema is None:
2288 # This can happen when building schemas for models with not-yet-defined references.
2289 # It may be a good idea to do a recursive pass at the end of the generation to remove
2290 # any redundant override keys.
2291 return json_schema
2292 for k, v in list(json_schema.items()):
2293 if k == '$ref':
2294 continue
2295 if k in referenced_json_schema and referenced_json_schema[k] == v:
2296 del json_schema[k] # redundant key
2297
2298 return json_schema
2299
2300 def get_schema_from_definitions(self, json_ref: JsonRef) -> JsonSchemaValue | None:
2301 try:
2302 def_ref = self.json_to_defs_refs[json_ref]
2303 if def_ref in self._core_defs_invalid_for_json_schema:
2304 raise self._core_defs_invalid_for_json_schema[def_ref]
2305 return self.definitions.get(def_ref, None)
2306 except KeyError:
2307 if json_ref.startswith(('http://', 'https://')):
2308 return None
2309 raise
2310
2311 def encode_default(self, dft: Any) -> Any:
2312 """Encode a default value to a JSON-serializable value.
2313
2314 This is used to encode default values for fields in the generated JSON schema.
2315
2316 Args:
2317 dft: The default value to encode.
2318
2319 Returns:
2320 The encoded default value.
2321 """
2322 from .type_adapter import TypeAdapter, _type_has_config
2323
2324 config = self._config
2325 try:
2326 default = (
2327 dft
2328 if _type_has_config(type(dft))
2329 else TypeAdapter(type(dft), config=config.config_dict).dump_python(
2330 dft, by_alias=self.by_alias, mode='json'
2331 )
2332 )
2333 except PydanticSchemaGenerationError:
2334 raise pydantic_core.PydanticSerializationError(f'Unable to encode default value {dft}')
2335
2336 return pydantic_core.to_jsonable_python(
2337 default, timedelta_mode=config.ser_json_timedelta, bytes_mode=config.ser_json_bytes, by_alias=self.by_alias
2338 )
2339
2340 def update_with_validations(
2341 self, json_schema: JsonSchemaValue, core_schema: CoreSchema, mapping: dict[str, str]
2342 ) -> None:
2343 """Update the json_schema with the corresponding validations specified in the core_schema,
2344 using the provided mapping to translate keys in core_schema to the appropriate keys for a JSON schema.
2345
2346 Args:
2347 json_schema: The JSON schema to update.
2348 core_schema: The core schema to get the validations from.
2349 mapping: A mapping from core_schema attribute names to the corresponding JSON schema attribute names.
2350 """
2351 for core_key, json_schema_key in mapping.items():
2352 if core_key in core_schema:
2353 json_schema[json_schema_key] = core_schema[core_key]
2354
2355 class ValidationsMapping:
2356 """This class just contains mappings from core_schema attribute names to the corresponding
2357 JSON schema attribute names. While I suspect it is unlikely to be necessary, you can in
2358 principle override this class in a subclass of GenerateJsonSchema (by inheriting from
2359 GenerateJsonSchema.ValidationsMapping) to change these mappings.
2360 """
2361
2362 numeric = {
2363 'multiple_of': 'multipleOf',
2364 'le': 'maximum',
2365 'ge': 'minimum',
2366 'lt': 'exclusiveMaximum',
2367 'gt': 'exclusiveMinimum',
2368 }
2369 bytes = {
2370 'min_length': 'minLength',
2371 'max_length': 'maxLength',
2372 }
2373 string = {
2374 'min_length': 'minLength',
2375 'max_length': 'maxLength',
2376 'pattern': 'pattern',
2377 }
2378 array = {
2379 'min_length': 'minItems',
2380 'max_length': 'maxItems',
2381 }
2382 object = {
2383 'min_length': 'minProperties',
2384 'max_length': 'maxProperties',
2385 }
2386
2387 def get_flattened_anyof(self, schemas: list[JsonSchemaValue]) -> JsonSchemaValue:
2388 members = []
2389 for schema in schemas:
2390 if len(schema) == 1 and 'anyOf' in schema:
2391 members.extend(schema['anyOf'])
2392 else:
2393 members.append(schema)
2394 members = _deduplicate_schemas(members)
2395 if len(members) == 1:
2396 return members[0]
2397 return {'anyOf': members}
2398
2399 def get_json_ref_counts(self, json_schema: JsonSchemaValue) -> dict[JsonRef, int]:
2400 """Get all values corresponding to the key '$ref' anywhere in the json_schema."""
2401 json_refs: dict[JsonRef, int] = Counter()
2402
2403 def _add_json_refs(schema: Any) -> None:
2404 if isinstance(schema, dict):
2405 if '$ref' in schema:
2406 json_ref = JsonRef(schema['$ref'])
2407 if not isinstance(json_ref, str):
2408 return # in this case, '$ref' might have been the name of a property
2409 already_visited = json_ref in json_refs
2410 json_refs[json_ref] += 1
2411 if already_visited:
2412 return # prevent recursion on a definition that was already visited
2413 try:
2414 defs_ref = self.json_to_defs_refs[json_ref]
2415 if defs_ref in self._core_defs_invalid_for_json_schema:
2416 raise self._core_defs_invalid_for_json_schema[defs_ref]
2417 _add_json_refs(self.definitions[defs_ref])
2418 except KeyError:
2419 if not json_ref.startswith(('http://', 'https://')):
2420 raise
2421
2422 for k, v in schema.items():
2423 if k == 'examples' and isinstance(v, list):
2424 # Skip examples that may contain arbitrary values and references
2425 # (see the comment in `_get_all_json_refs` for more details).
2426 continue
2427 _add_json_refs(v)
2428 elif isinstance(schema, list):
2429 for v in schema:
2430 _add_json_refs(v)
2431
2432 _add_json_refs(json_schema)
2433 return json_refs
2434
2435 def handle_invalid_for_json_schema(self, schema: CoreSchemaOrField, error_info: str) -> JsonSchemaValue:
2436 raise PydanticInvalidForJsonSchema(f'Cannot generate a JsonSchema for {error_info}')
2437
2438 def emit_warning(self, kind: JsonSchemaWarningKind, detail: str) -> None:
2439 """This method simply emits PydanticJsonSchemaWarnings based on handling in the `warning_message` method."""
2440 message = self.render_warning_message(kind, detail)
2441 if message is not None:
2442 warnings.warn(message, PydanticJsonSchemaWarning)
2443
2444 def render_warning_message(self, kind: JsonSchemaWarningKind, detail: str) -> str | None:
2445 """This method is responsible for ignoring warnings as desired, and for formatting the warning messages.
2446
2447 You can override the value of `ignored_warning_kinds` in a subclass of GenerateJsonSchema
2448 to modify what warnings are generated. If you want more control, you can override this method;
2449 just return None in situations where you don't want warnings to be emitted.
2450
2451 Args:
2452 kind: The kind of warning to render. It can be one of the following:
2453
2454 - 'skipped-choice': A choice field was skipped because it had no valid choices.
2455 - 'non-serializable-default': A default value was skipped because it was not JSON-serializable.
2456 detail: A string with additional details about the warning.
2457
2458 Returns:
2459 The formatted warning message, or `None` if no warning should be emitted.
2460 """
2461 if kind in self.ignored_warning_kinds:
2462 return None
2463 return f'{detail} [{kind}]'
2464
2465 def _build_definitions_remapping(self) -> _DefinitionsRemapping:
2466 defs_to_json: dict[DefsRef, JsonRef] = {}
2467 for defs_refs in self._prioritized_defsref_choices.values():
2468 for defs_ref in defs_refs:
2469 json_ref = JsonRef(self.ref_template.format(model=defs_ref))
2470 defs_to_json[defs_ref] = json_ref
2471
2472 return _DefinitionsRemapping.from_prioritized_choices(
2473 self._prioritized_defsref_choices, defs_to_json, self.definitions
2474 )
2475
2476 def _garbage_collect_definitions(self, schema: JsonSchemaValue) -> None:
2477 visited_defs_refs: set[DefsRef] = set()
2478 unvisited_json_refs = _get_all_json_refs(schema)
2479 while unvisited_json_refs:
2480 next_json_ref = unvisited_json_refs.pop()
2481 try:
2482 next_defs_ref = self.json_to_defs_refs[next_json_ref]
2483 if next_defs_ref in visited_defs_refs:
2484 continue
2485 visited_defs_refs.add(next_defs_ref)
2486 unvisited_json_refs.update(_get_all_json_refs(self.definitions[next_defs_ref]))
2487 except KeyError:
2488 if not next_json_ref.startswith(('http://', 'https://')):
2489 raise
2490
2491 self.definitions = {k: v for k, v in self.definitions.items() if k in visited_defs_refs}
2492
2493
2494# ##### Start JSON Schema Generation Functions #####
2495
2496
2497def model_json_schema(
2498 cls: type[BaseModel] | type[PydanticDataclass],
2499 by_alias: bool = True,
2500 ref_template: str = DEFAULT_REF_TEMPLATE,
2501 union_format: Literal['any_of', 'primitive_type_array'] = 'any_of',
2502 schema_generator: type[GenerateJsonSchema] = GenerateJsonSchema,
2503 mode: JsonSchemaMode = 'validation',
2504) -> dict[str, Any]:
2505 """Utility function to generate a JSON Schema for a model.
2506
2507 Args:
2508 cls: The model class to generate a JSON Schema for.
2509 by_alias: If `True` (the default), fields will be serialized according to their alias.
2510 If `False`, fields will be serialized according to their attribute name.
2511 ref_template: The template to use for generating JSON Schema references.
2512 union_format: The format to use when combining schemas from unions together. Can be one of:
2513
2514 - `'any_of'`: Use the [`anyOf`](https://json-schema.org/understanding-json-schema/reference/combining#anyOf)
2515 keyword to combine schemas (the default).
2516 - `'primitive_type_array'`: Use the [`type`](https://json-schema.org/understanding-json-schema/reference/type)
2517 keyword as an array of strings, containing each type of the combination. If any of the schemas is not a primitive
2518 type (`string`, `boolean`, `null`, `integer` or `number`) or contains constraints/metadata, falls back to
2519 `any_of`.
2520 schema_generator: The class to use for generating the JSON Schema.
2521 mode: The mode to use for generating the JSON Schema. It can be one of the following:
2522
2523 - 'validation': Generate a JSON Schema for validating data.
2524 - 'serialization': Generate a JSON Schema for serializing data.
2525
2526 Returns:
2527 The generated JSON Schema.
2528 """
2529 from .main import BaseModel
2530
2531 schema_generator_instance = schema_generator(
2532 by_alias=by_alias, ref_template=ref_template, union_format=union_format
2533 )
2534
2535 if isinstance(cls.__pydantic_core_schema__, _mock_val_ser.MockCoreSchema):
2536 cls.__pydantic_core_schema__.rebuild()
2537
2538 if cls is BaseModel:
2539 raise AttributeError('model_json_schema() must be called on a subclass of BaseModel, not BaseModel itself.')
2540
2541 assert not isinstance(cls.__pydantic_core_schema__, _mock_val_ser.MockCoreSchema), 'this is a bug! please report it'
2542 return schema_generator_instance.generate(cls.__pydantic_core_schema__, mode=mode)
2543
2544
2545def models_json_schema(
2546 models: Sequence[tuple[type[BaseModel] | type[PydanticDataclass], JsonSchemaMode]],
2547 *,
2548 by_alias: bool = True,
2549 title: str | None = None,
2550 description: str | None = None,
2551 ref_template: str = DEFAULT_REF_TEMPLATE,
2552 union_format: Literal['any_of', 'primitive_type_array'] = 'any_of',
2553 schema_generator: type[GenerateJsonSchema] = GenerateJsonSchema,
2554) -> tuple[dict[tuple[type[BaseModel] | type[PydanticDataclass], JsonSchemaMode], JsonSchemaValue], JsonSchemaValue]:
2555 """Utility function to generate a JSON Schema for multiple models.
2556
2557 Args:
2558 models: A sequence of tuples of the form (model, mode).
2559 by_alias: Whether field aliases should be used as keys in the generated JSON Schema.
2560 title: The title of the generated JSON Schema.
2561 description: The description of the generated JSON Schema.
2562 ref_template: The reference template to use for generating JSON Schema references.
2563 union_format: The format to use when combining schemas from unions together. Can be one of:
2564
2565 - `'any_of'`: Use the [`anyOf`](https://json-schema.org/understanding-json-schema/reference/combining#anyOf)
2566 keyword to combine schemas (the default).
2567 - `'primitive_type_array'`: Use the [`type`](https://json-schema.org/understanding-json-schema/reference/type)
2568 keyword as an array of strings, containing each type of the combination. If any of the schemas is not a primitive
2569 type (`string`, `boolean`, `null`, `integer` or `number`) or contains constraints/metadata, falls back to
2570 `any_of`.
2571 schema_generator: The schema generator to use for generating the JSON Schema.
2572
2573 Returns:
2574 A tuple where:
2575 - The first element is a dictionary whose keys are tuples of JSON schema key type and JSON mode, and
2576 whose values are the JSON schema corresponding to that pair of inputs. (These schemas may have
2577 JsonRef references to definitions that are defined in the second returned element.)
2578 - The second element is a JSON schema containing all definitions referenced in the first returned
2579 element, along with the optional title and description keys.
2580 """
2581 for cls, _ in models:
2582 if isinstance(cls.__pydantic_core_schema__, _mock_val_ser.MockCoreSchema):
2583 cls.__pydantic_core_schema__.rebuild()
2584
2585 instance = schema_generator(by_alias=by_alias, ref_template=ref_template, union_format=union_format)
2586 inputs: list[tuple[type[BaseModel] | type[PydanticDataclass], JsonSchemaMode, CoreSchema]] = [
2587 (m, mode, m.__pydantic_core_schema__) for m, mode in models
2588 ]
2589 json_schemas_map, definitions = instance.generate_definitions(inputs)
2590
2591 json_schema: dict[str, Any] = {}
2592 if definitions:
2593 json_schema['$defs'] = definitions
2594 if title:
2595 json_schema['title'] = title
2596 if description:
2597 json_schema['description'] = description
2598
2599 return json_schemas_map, json_schema
2600
2601
2602# ##### End JSON Schema Generation Functions #####
2603
2604
2605_HashableJsonValue: TypeAlias = Union[
2606 int, float, str, bool, None, tuple['_HashableJsonValue', ...], tuple[tuple[str, '_HashableJsonValue'], ...]
2607]
2608
2609
2610def _deduplicate_schemas(schemas: Iterable[JsonDict]) -> list[JsonDict]:
2611 return list({_make_json_hashable(schema): schema for schema in schemas}.values())
2612
2613
2614def _make_json_hashable(value: JsonValue) -> _HashableJsonValue:
2615 if isinstance(value, dict):
2616 return tuple(sorted((k, _make_json_hashable(v)) for k, v in value.items()))
2617 elif isinstance(value, list):
2618 return tuple(_make_json_hashable(v) for v in value)
2619 else:
2620 return value
2621
2622
2623@dataclasses.dataclass(**_internal_dataclass.slots_true)
2624class WithJsonSchema:
2625 """!!! abstract "Usage Documentation"
2626 [`WithJsonSchema` Annotation](../concepts/json_schema.md#withjsonschema-annotation)
2627
2628 Add this as an annotation on a field to override the (base) JSON schema that would be generated for that field.
2629 This provides a way to set a JSON schema for types that would otherwise raise errors when producing a JSON schema,
2630 such as Callable, or types that have an is-instance core schema, without needing to go so far as creating a
2631 custom subclass of pydantic.json_schema.GenerateJsonSchema.
2632 Note that any _modifications_ to the schema that would normally be made (such as setting the title for model fields)
2633 will still be performed.
2634
2635 If `mode` is set this will only apply to that schema generation mode, allowing you
2636 to set different json schemas for validation and serialization.
2637 """
2638
2639 json_schema: JsonSchemaValue | None
2640 mode: Literal['validation', 'serialization'] | None = None
2641
2642 def __get_pydantic_json_schema__(
2643 self, core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler
2644 ) -> JsonSchemaValue:
2645 mode = self.mode or handler.mode
2646 if mode != handler.mode:
2647 return handler(core_schema)
2648 if self.json_schema is None:
2649 # This exception is handled in pydantic.json_schema.GenerateJsonSchema._named_required_fields_schema
2650 raise PydanticOmit
2651 else:
2652 return self.json_schema.copy()
2653
2654 def __hash__(self) -> int:
2655 return hash(type(self.mode))
2656
2657
2658class Examples:
2659 """Add examples to a JSON schema.
2660
2661 If the JSON Schema already contains examples, the provided examples
2662 will be appended.
2663
2664 If `mode` is set this will only apply to that schema generation mode,
2665 allowing you to add different examples for validation and serialization.
2666 """
2667
2668 @overload
2669 @deprecated('Using a dict for `examples` is deprecated since v2.9 and will be removed in v3.0. Use a list instead.')
2670 def __init__(
2671 self, examples: dict[str, Any], mode: Literal['validation', 'serialization'] | None = None
2672 ) -> None: ...
2673
2674 @overload
2675 def __init__(self, examples: list[Any], mode: Literal['validation', 'serialization'] | None = None) -> None: ...
2676
2677 def __init__(
2678 self, examples: dict[str, Any] | list[Any], mode: Literal['validation', 'serialization'] | None = None
2679 ) -> None:
2680 if isinstance(examples, dict):
2681 warnings.warn(
2682 'Using a dict for `examples` is deprecated, use a list instead.',
2683 PydanticDeprecatedSince29,
2684 stacklevel=2,
2685 )
2686 self.examples = examples
2687 self.mode = mode
2688
2689 def __get_pydantic_json_schema__(
2690 self, core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler
2691 ) -> JsonSchemaValue:
2692 mode = self.mode or handler.mode
2693 json_schema = handler(core_schema)
2694 if mode != handler.mode:
2695 return json_schema
2696 examples = json_schema.get('examples')
2697 if examples is None:
2698 json_schema['examples'] = to_jsonable_python(self.examples)
2699 if isinstance(examples, dict):
2700 if isinstance(self.examples, list):
2701 warnings.warn(
2702 'Updating existing JSON Schema examples of type dict with examples of type list. '
2703 'Only the existing examples values will be retained. Note that dict support for '
2704 'examples is deprecated and will be removed in v3.0.',
2705 UserWarning,
2706 )
2707 json_schema['examples'] = to_jsonable_python(
2708 [ex for value in examples.values() for ex in value] + self.examples
2709 )
2710 else:
2711 json_schema['examples'] = to_jsonable_python({**examples, **self.examples})
2712 if isinstance(examples, list):
2713 if isinstance(self.examples, list):
2714 json_schema['examples'] = to_jsonable_python(examples + self.examples)
2715 elif isinstance(self.examples, dict):
2716 warnings.warn(
2717 'Updating existing JSON Schema examples of type list with examples of type dict. '
2718 'Only the examples values will be retained. Note that dict support for '
2719 'examples is deprecated and will be removed in v3.0.',
2720 UserWarning,
2721 )
2722 json_schema['examples'] = to_jsonable_python(
2723 examples + [ex for value in self.examples.values() for ex in value]
2724 )
2725
2726 return json_schema
2727
2728 def __hash__(self) -> int:
2729 return hash(type(self.mode))
2730
2731
2732def _get_all_json_refs(item: Any) -> set[JsonRef]:
2733 """Get all the definitions references from a JSON schema."""
2734 refs: set[JsonRef] = set()
2735 stack = [item]
2736
2737 while stack:
2738 current = stack.pop()
2739 if isinstance(current, dict):
2740 for key, value in current.items():
2741 if key == 'examples' and isinstance(value, list):
2742 # Skip examples that may contain arbitrary values and references
2743 # (e.g. `{"examples": [{"$ref": "..."}]}`). Note: checking for value
2744 # of type list is necessary to avoid skipping valid portions of the schema,
2745 # for instance when "examples" is used as a property key. A more robust solution
2746 # could be found, but would require more advanced JSON Schema parsing logic.
2747 continue
2748 if key == '$ref' and isinstance(value, str):
2749 refs.add(JsonRef(value))
2750 elif isinstance(value, dict):
2751 stack.append(value)
2752 elif isinstance(value, list):
2753 stack.extend(value)
2754 elif isinstance(current, list):
2755 stack.extend(current)
2756
2757 return refs
2758
2759
2760AnyType = TypeVar('AnyType')
2761
2762if TYPE_CHECKING:
2763 SkipJsonSchema = Annotated[AnyType, ...]
2764else:
2765
2766 @dataclasses.dataclass(**_internal_dataclass.slots_true)
2767 class SkipJsonSchema:
2768 """!!! abstract "Usage Documentation"
2769 [`SkipJsonSchema` Annotation](../concepts/json_schema.md#skipjsonschema-annotation)
2770
2771 Add this as an annotation on a field to skip generating a JSON schema for that field.
2772
2773 Example:
2774 ```python
2775 from pprint import pprint
2776 from typing import Union
2777
2778 from pydantic import BaseModel
2779 from pydantic.json_schema import SkipJsonSchema
2780
2781 class Model(BaseModel):
2782 a: Union[int, None] = None # (1)!
2783 b: Union[int, SkipJsonSchema[None]] = None # (2)!
2784 c: SkipJsonSchema[Union[int, None]] = None # (3)!
2785
2786 pprint(Model.model_json_schema())
2787 '''
2788 {
2789 'properties': {
2790 'a': {
2791 'anyOf': [
2792 {'type': 'integer'},
2793 {'type': 'null'}
2794 ],
2795 'default': None,
2796 'title': 'A'
2797 },
2798 'b': {
2799 'default': None,
2800 'title': 'B',
2801 'type': 'integer'
2802 }
2803 },
2804 'title': 'Model',
2805 'type': 'object'
2806 }
2807 '''
2808 ```
2809
2810 1. The integer and null types are both included in the schema for `a`.
2811 2. The integer type is the only type included in the schema for `b`.
2812 3. The entirety of the `c` field is omitted from the schema.
2813 """
2814
2815 def __class_getitem__(cls, item: AnyType) -> AnyType:
2816 return Annotated[item, cls()]
2817
2818 def __get_pydantic_json_schema__(
2819 self, core_schema: CoreSchema, handler: GetJsonSchemaHandler
2820 ) -> JsonSchemaValue:
2821 raise PydanticOmit
2822
2823 def __hash__(self) -> int:
2824 return hash(type(self))
2825
2826
2827def _get_typed_dict_config(cls: type[Any] | None) -> ConfigDict:
2828 if cls is not None:
2829 try:
2830 return _decorators.get_attribute_from_bases(cls, '__pydantic_config__')
2831 except AttributeError:
2832 pass
2833 return {}
2834
2835
2836def _get_ser_schema_for_default_value(schema: CoreSchema) -> core_schema.PlainSerializerFunctionSerSchema | None:
2837 """Get a `'function-plain'` serialization schema that can be used to serialize a default value.
2838
2839 This takes into account having the serialization schema nested under validation schema(s).
2840 """
2841 if (
2842 (ser_schema := schema.get('serialization'))
2843 and ser_schema['type'] == 'function-plain'
2844 and not ser_schema.get('info_arg')
2845 ):
2846 return ser_schema
2847 if _core_utils.is_function_with_inner_schema(schema):
2848 return _get_ser_schema_for_default_value(schema['schema'])