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