diff --git a/docs/concepts/fields.md b/docs/concepts/fields.md index 448902640b..8195f77d25 100644 --- a/docs/concepts/fields.md +++ b/docs/concepts/fields.md @@ -1,7 +1,7 @@ ??? api "API Documentation" [`pydantic.fields.Field`][pydantic.fields.Field]
-The `Field` function is used to customize and add metadata to fields of models. +The [`Field`][pydantic.fields.Field] function is used to customize and add metadata to fields of models. ## Default values @@ -40,7 +40,7 @@ class User(BaseModel): ## Using `Annotated` -The `Field` function can also be used together with `Annotated`. +The [`Field`][pydantic.fields.Field] function can also be used together with [`Annotated`][annotated]. ```py from uuid import uuid4 @@ -54,6 +54,11 @@ class User(BaseModel): id: Annotated[str, Field(default_factory=lambda: uuid4().hex)] ``` +!!! note + Defaults can be set outside [`Annotated`][annotated] as the assigned value or with `Field.default_factory` inside + [`Annotated`][annotated]. The `Field.default` argument is not supported inside [`Annotated`][annotated]. + + ## Field aliases For validation and serialization, you can define an alias for a field. @@ -679,7 +684,7 @@ See the [Discriminated Unions] docs for more details. ## Strict Mode -The `strict` parameter on a `Field` specifies whether the field should be validated in "strict mode". +The `strict` parameter on a [`Field`][pydantic.fields.Field] specifies whether the field should be validated in "strict mode". In strict mode, Pydantic throws an error during validation instead of coercing data on the field where `strict=True`. ```py @@ -761,62 +766,43 @@ See the [Serialization] section for more details. ## Customizing JSON Schema -There are fields that exclusively to customise the generated JSON Schema: +Some field parameters are used exclusively to customize the generated JSON schema. The parameters in question are: + +* `title` +* `description` +* `examples` +* `json_schema_extra` + +Read more about JSON schema customization / modification with fields in the [Customizing JSON Schema] section of the JSON schema docs. -* `title`: The title of the field. -* `description`: The description of the field. -* `examples`: The examples of the field. -* `json_schema_extra`: Extra JSON Schema properties to be added to the field. +## The `computed_field` decorator + +??? api "API Documentation" + [`pydantic.fields.computed_field`][pydantic.fields.computed_field]
+ +The `computed_field` decorator can be used to include `property` or `cached_property` attributes when serializing a +model or dataclass. This can be useful for fields that are computed from other fields, or for fields that +are expensive to computed (and thus, are cached). Here's an example: ```py -from pydantic import BaseModel, EmailStr, Field, SecretStr +from pydantic import BaseModel, computed_field -class User(BaseModel): - age: int = Field(description='Age of the user') - email: EmailStr = Field(examples=['marcelo@mail.com']) - name: str = Field(title='Username') - password: SecretStr = Field( - json_schema_extra={ - 'title': 'Password', - 'description': 'Password of the user', - 'examples': ['123456'], - } - ) +class Box(BaseModel): + width: float + height: float + depth: float + @computed_field + def volume(self) -> float: + return self.width * self.height * self.depth -print(User.model_json_schema()) -""" -{ - 'properties': { - 'age': { - 'description': 'Age of the user', - 'title': 'Age', - 'type': 'integer', - }, - 'email': { - 'examples': ['marcelo@mail.com'], - 'format': 'email', - 'title': 'Email', - 'type': 'string', - }, - 'name': {'title': 'Username', 'type': 'string'}, - 'password': { - 'description': 'Password of the user', - 'examples': ['123456'], - 'format': 'password', - 'title': 'Password', - 'type': 'string', - 'writeOnly': True, - }, - }, - 'required': ['age', 'email', 'name', 'password'], - 'title': 'User', - 'type': 'object', -} -""" + +b = Box(width=1, height=2, depth=3) +print(b.model_dump()) +#> {'width': 1.0, 'height': 2.0, 'depth': 3.0, 'volume': 6.0} ``` @@ -828,3 +814,5 @@ print(User.model_json_schema()) [frozen dataclass documentation]: https://docs.python.org/3/library/dataclasses.html#frozen-instances [Validate Assignment]: models.md#validate-assignment [Serialization]: serialization.md#model-and-field-level-include-and-exclude +[Customizing JSON Schema]: json_schema.md#field-level-customization +[annotated]: https://docs.python.org/3/library/typing.html#typing.Annotated diff --git a/docs/concepts/json_schema.md b/docs/concepts/json_schema.md index 919b2e2e9d..0e8d14669e 100644 --- a/docs/concepts/json_schema.md +++ b/docs/concepts/json_schema.md @@ -1,21 +1,46 @@ ??? api "API Documentation" [`pydantic.json_schema`][pydantic.json_schema]
-Pydantic allows automatic creation of JSON schemas from models. +Pydantic allows automatic creation and customization of JSON schemas from models. +The generated JSON schemas are compliant with the following specifications: -Using Pydantic, there are several ways to generate JSON schemas or JSON representations from fields or models: +* [JSON Schema Draft 2020-12](https://json-schema.org/draft/2020-12/release-notes.html) +* [OpenAPI Specification v3.1.0](https://github.com/OAI/OpenAPI-Specification). -* [`BaseModel.model_json_schema`][pydantic.main.BaseModel.model_json_schema] returns a jsonable dict of the schema. -* [`BaseModel.model_dump_json`][pydantic.main.BaseModel.model_dump_json] returns a JSON string representation of the - dict of the schema. -* [`TypeAdapter.dump_json`][pydantic.type_adapter.TypeAdapter.dump_json] serializes an instance of the adapted type to - JSON. -* [`TypeAdapter.json_schema`][pydantic.type_adapter.TypeAdapter.json_schema] generates a JSON schema for the adapted type. +## Generating JSON Schema -The generated JSON schemas are compliant with the following specifications: +Use the following functions to generate JSON schema: -* [JSON Schema Draft 2020-12](https://json-schema.org/draft/2020-12/release-notes.html) -* [OpenAPI extensions](https://github.com/OAI/OpenAPI-Specification). +* [`BaseModel.model_json_schema`][pydantic.main.BaseModel.model_json_schema] returns a jsonable dict of a model's schema. +* [`TypeAdapter.json_schema`][pydantic.type_adapter.TypeAdapter.json_schema] returns a jsonable dict of an adapted type's schema. + +!!! note + These methods are not to be confused with [`BaseModel.model_dump_json`][pydantic.main.BaseModel.model_dump_json] + and [`TypeAdapter.dump_json`][pydantic.type_adapter.TypeAdapter.dump_json], which serialize instances of the + model or adapted type, respectively. These methods return JSON strings. In comparison, + [`BaseModel.model_json_schema`][pydantic.main.BaseModel.model_json_schema] and + [`TypeAdapter.json_schema`][pydantic.type_adapter.TypeAdapter.json_schema] return a jsonable dict + representing the JSON schema of the model or adapted type, respectively. + +!!! note "on the "jsonable" nature of JSON schema" + Regarding the "jsonable" nature of the [`model_json_schema`][pydantic.main.BaseModel.model_json_schema] results, + calling `json.dumps(m.model_json_schema())`on some `BaseModel` `m` returns a valid JSON string. Similarly, for + [`TypeAdapter.json_schema`][pydantic.type_adapter.TypeAdapter.json_schema], calling + `json.dumps(TypeAdapter().json_schema())` returns a valid JSON string. + + +!!! tip + Pydantic offers support for both of: + + 1. [Customizing JSON Schema](#customizing-json-schema) + 2. [Customizing the JSON Schema Generation Process](#customizing-the-json-schema-generation-process) + + The first approach generally has a more narrow scope, allowing for customization of the JSON schema for + more specific cases and types. The second approach generally has a more broad scope, allowing for customization + of the JSON schema generation process overall. The same effects can be achieved with either approach, but + depending on your use case, one approach might offer a more simple solution than the other. + +Here's an example of generating JSON schema from a `BaseModel`: ```py output="json" import json @@ -58,7 +83,8 @@ class MainModel(BaseModel): ) -print(json.dumps(MainModel.model_json_schema(), indent=2)) +main_model_schema = MainModel.model_json_schema() # (1)! +print(json.dumps(main_model_schema, indent=2)) # (2)! """ { "$defs": { @@ -132,36 +158,16 @@ print(json.dumps(MainModel.model_json_schema(), indent=2)) """ ``` -## General notes on JSON schema generation - -* The JSON schema for `Optional` fields indicates that the value `null` is allowed. -* The `Decimal` type is exposed in JSON schema (and serialized) as a string. -* The JSON schema does not preserve `namedtuple`s as `namedtuple`s. -* When they differ, you can specify whether you want the JSON schema to represent the inputs to validation - or the outputs from serialization. -* Sub-models used are added to the `$defs` JSON attribute and referenced, as per the spec. -* Sub-models with modifications (via the `Field` class) like a custom title, description, or default value, - are recursively included instead of referenced. -* The `description` for models is taken from either the docstring of the class or the argument `description` to - the `Field` class. -* The schema is generated by default using aliases as keys, but it can be generated using model - property names instead by calling [`model_json_schema()`][pydantic.main.BaseModel.model_json_schema] or - [`model_dump_json()`][pydantic.main.BaseModel.model_dump_json] with the `by_alias=False` keyword argument. -* The format of `$ref`s can be altered by calling [`model_json_schema()`][pydantic.main.BaseModel.model_json_schema] - or `model_dump_json()`][pydantic.main.BaseModel.model_dump_json] with the `ref_template` keyword argument. - -!!! note - Regarding the "jsonable" nature of the [`model_json_schema`][pydantic.main.BaseModel.model_json_schema] results, - calling `json.dumps(m.model_json_schema())`on some `BaseModel` `m` returns a valid JSON string. - - -## Getting schema of a specified type +1. This produces a "jsonable" dict of `MainModel`'s schema. +2. Calling `json.dumps` on the schema dict produces a JSON string. The [`TypeAdapter`][pydantic.type_adapter.TypeAdapter] class lets you create an object with methods for validating, serializing, and producing JSON schemas for arbitrary types. This serves as a complete replacement for `schema_of` in Pydantic V1 (which is now deprecated). -```python +Here's an example of generating JSON schema from a [`TypeAdapter`][pydantic.type_adapter.TypeAdapter]: + +```py from typing import List from pydantic import TypeAdapter @@ -171,29 +177,230 @@ print(adapter.json_schema()) #> {'items': {'type': 'integer'}, 'type': 'array'} ``` -## Field customization +You can also generate JSON schemas for combinations of [`BaseModel`s][pydantic.main.BaseModel] +and [`TypeAdapter`s][pydantic.type_adapter.TypeAdapter], as shown in this example: + +```py output="json" +import json +from typing import Union + +from pydantic import BaseModel, TypeAdapter + + +class Cat(BaseModel): + name: str + color: str + + +class Dog(BaseModel): + name: str + breed: str + + +ta = TypeAdapter(Union[Cat, Dog]) +ta_schema = ta.json_schema() +print(json.dumps(ta_schema, indent=2)) +""" +{ + "$defs": { + "Cat": { + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "color": { + "title": "Color", + "type": "string" + } + }, + "required": [ + "name", + "color" + ], + "title": "Cat", + "type": "object" + }, + "Dog": { + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "breed": { + "title": "Breed", + "type": "string" + } + }, + "required": [ + "name", + "breed" + ], + "title": "Dog", + "type": "object" + } + }, + "anyOf": [ + { + "$ref": "#/$defs/Cat" + }, + { + "$ref": "#/$defs/Dog" + } + ] +} +""" +``` + +### Configuring the `JsonSchemaMode` + +Specify the mode of JSON schema generation via the `mode` parameter in the +[`model_json_schema`][pydantic.main.BaseModel.model_json_schema] and +[`TypeAdapter.json_schema`][pydantic.type_adapter.TypeAdapter.json_schema] methods. By default, the mode is set to +`'validation'`, which produces a JSON schema corresponding to the model's validation schema. + +The [`JsonSchemaMode`][pydantic.json_schema.JsonSchemaMode] is a type alias that represents the available options for the `mode` parameter: + +* `'validation'` +* `'serialization'` + +Here's an example of how to specify the `mode` parameter, and how it affects the generated JSON schema: + +```py +from decimal import Decimal + +from pydantic import BaseModel + + +class Model(BaseModel): + a: Decimal = Decimal('12.34') + + +print(Model.model_json_schema(mode='validation')) +""" +{ + 'properties': { + 'a': { + 'anyOf': [{'type': 'number'}, {'type': 'string'}], + 'default': '12.34', + 'title': 'A', + } + }, + 'title': 'Model', + 'type': 'object', +} +""" + +print(Model.model_json_schema(mode='serialization')) +""" +{ + 'properties': {'a': {'default': '12.34', 'title': 'A', 'type': 'string'}}, + 'title': 'Model', + 'type': 'object', +} +""" +``` + + +## Customizing JSON Schema + +The generated JSON schema can be customized at both the field level and model level via: + +1. [Field-level customization](#field-level-customization) with the [`Field`][pydantic.fields.Field] constructor +2. [Model-level customization](#model-level-customization) with [`model_config`][pydantic.config.ConfigDict] + +At both the field and model levels, you can use the `json_schema_extra` option to add extra information to the JSON schema. +The [Using `json_schema_extra`](#using-json_schema_extra) section below provides more details on this option. + +For custom types, Pydantic offers other tools for customizing JSON schema generation: + +1. [`WithJsonSchema` annotation](#withjsonschema-annotation) +2. [`SkipJsonSchema` annotation](#skipjsonschema-annotation) +3. [Implementing `__get_pydantic_core_schema__`](#implementing-getpydanticcoreschema) +4. [Implementing `__get_pydantic_json_schema__`](#implementing-getpydanticjsonschema) + +### Field-Level Customization Optionally, the [`Field`][pydantic.fields.Field] function can be used to provide extra information about the field and validations. -See [Customizing JSON Schema](fields.md#customizing-json-schema) for details on field parameters that are used -exclusively to customize the generated JSON schema. +Some field parameters are used exclusively to customize the generated JSON Schema: -You can also use [model config][pydantic.config.ConfigDict] to customize JSON serialization and extra schema properties on a model. -Specifically, the following config options are relevant: +* `title`: The title of the field. +* `description`: The description of the field. +* `examples`: The examples of the field. +* `json_schema_extra`: Extra JSON Schema properties to be added to the field. -* [`title`][pydantic.config.ConfigDict.title] -* [`use_enum_values`][pydantic.config.ConfigDict.use_enum_values] -* [`json_schema_extra`][pydantic.config.ConfigDict.json_schema_extra] -* [`ser_json_timedelta`][pydantic.config.ConfigDict.ser_json_timedelta] -* [`ser_json_bytes`][pydantic.config.ConfigDict.ser_json_bytes] -* [`ser_json_inf_nan`][pydantic.config.ConfigDict.ser_json_inf_nan] +Here's an example: + +```py output="json" +import json + +from pydantic import BaseModel, EmailStr, Field, SecretStr + + +class User(BaseModel): + age: int = Field(description='Age of the user') + email: EmailStr = Field(examples=['marcelo@mail.com']) + name: str = Field(title='Username') + password: SecretStr = Field( + json_schema_extra={ + 'title': 'Password', + 'description': 'Password of the user', + 'examples': ['123456'], + } + ) + + +print(json.dumps(User.model_json_schema(), indent=2)) +""" +{ + "properties": { + "age": { + "description": "Age of the user", + "title": "Age", + "type": "integer" + }, + "email": { + "examples": [ + "marcelo@mail.com" + ], + "format": "email", + "title": "Email", + "type": "string" + }, + "name": { + "title": "Username", + "type": "string" + }, + "password": { + "description": "Password of the user", + "examples": [ + "123456" + ], + "format": "password", + "title": "Password", + "type": "string", + "writeOnly": true + } + }, + "required": [ + "age", + "email", + "name", + "password" + ], + "title": "User", + "type": "object" +} +""" +``` -### Unenforced `Field` constraints +#### Unenforced `Field` constraints If Pydantic finds constraints which are not being enforced, an error will be raised. If you want to force the constraint to appear in the schema, even though it's not being checked upon parsing, you can use variadic arguments -to `Field` with the raw schema attribute name: +to [`Field`][pydantic.fields.Field] with the raw schema attribute name: ```py from pydantic import BaseModel, Field, PositiveInt @@ -235,11 +442,10 @@ print(ModelB.model_json_schema()) """ ``` -### typing.Annotated Fields - -Rather than assigning a `Field` value, it can be specified in the type hint with `typing.Annotated`: +You can specify JSON schema modifications via the [`Field`][pydantic.fields.Field] constructor via `typing.Annotated` as well: -```py +```py output="json" +import json from uuid import uuid4 from typing_extensions import Annotated @@ -249,34 +455,200 @@ from pydantic import BaseModel, Field class Foo(BaseModel): id: Annotated[str, Field(default_factory=lambda: uuid4().hex)] - name: Annotated[str, Field(max_length=256)] = Field('Bar', title='te') + name: Annotated[str, Field(max_length=256)] = Field( + 'Bar', title='CustomName' + ) -print(Foo.model_json_schema()) +print(json.dumps(Foo.model_json_schema(), indent=2)) """ { - 'properties': { - 'id': {'title': 'Id', 'type': 'string'}, - 'name': { - 'default': 'Bar', - 'maxLength': 256, - 'title': 'te', - 'type': 'string', - }, + "properties": { + "id": { + "title": "Id", + "type": "string" }, - 'title': 'Foo', - 'type': 'object', + "name": { + "default": "Bar", + "maxLength": 256, + "title": "CustomName", + "type": "string" + } + }, + "title": "Foo", + "type": "object" +} +""" +``` + +### Model-Level Customization + +You can also use [model config][pydantic.config.ConfigDict] to customize JSON schema generation on a model. +Specifically, the following config options are relevant: + +* [`title`][pydantic.config.ConfigDict.title] +* [`json_schema_extra`][pydantic.config.ConfigDict.json_schema_extra] +* [`schema_generator`][pydantic.config.ConfigDict.schema_generator] +* [`json_schema_mode_override`][pydantic.config.ConfigDict.json_schema_mode_override] + +### Using `json_schema_extra` + +The `json_schema_extra` option can be used to add extra information to the JSON schema, either at the +[Field level](#field-level-customization) or at the [Model level](#model-level-customization). +You can pass a `dict` or a `Callable` to `json_schema_extra`. + +#### Using `json_schema_extra` with a `dict` + +You can pass a `dict` to `json_schema_extra` to add extra information to the JSON schema: + +```py output="json" +import json + +from pydantic import BaseModel, ConfigDict + + +class Model(BaseModel): + a: str + + model_config = ConfigDict(json_schema_extra={'examples': [{'a': 'Foo'}]}) + + +print(json.dumps(Model.model_json_schema(), indent=2)) +""" +{ + "examples": [ + { + "a": "Foo" + } + ], + "properties": { + "a": { + "title": "A", + "type": "string" + } + }, + "required": [ + "a" + ], + "title": "Model", + "type": "object" +} +""" +``` + +#### Using `json_schema_extra` with a `Callable` + +You can pass a `Callable` to `json_schema_extra` to modify the JSON schema with a function: + +```py output="json" +import json + +from pydantic import BaseModel, Field + + +def pop_default(s): + s.pop('default') + + +class Model(BaseModel): + a: int = Field(default=1, json_schema_extra=pop_default) + + +print(json.dumps(Model.model_json_schema(), indent=2)) +""" +{ + "properties": { + "a": { + "title": "A", + "type": "integer" + } + }, + "title": "Model", + "type": "object" +} +""" +``` + +### `WithJsonSchema` annotation + +??? api "API Documentation" + [`pydantic.json_schema.WithJsonSchema`][pydantic.json_schema.WithJsonSchema]
+ +!!! tip + Using [`WithJsonSchema`][pydantic.json_schema.WithJsonSchema]] is preferred over + [implementing `__get_pydantic_json_schema__`](#implementing-getpydanticjsonschema) for custom types, + as it's more simple and less error-prone. + +The [`WithJsonSchema`][pydantic.json_schema.WithJsonSchema] annotation can be used to override the generated (base) +JSON schema for a given type without the need to implement `__get_pydantic_core_schema__` +or `__get_pydantic_json_schema__` on the type itself. + +This provides a way to set a JSON schema for types that would otherwise raise errors when producing a JSON schema, +such as `Callable`, or types that have an [`is-instance`][pydantic_core.core_schema.is_instance_schema] core schema. + +For example, the use of a [`PlainValidator`][pydantic.functional_validators.PlainValidator] in the following example +would otherwise raise an error when producing a JSON schema because the [`PlainValidator`][pydantic.functional_validators.PlainValidator] +is a `Callable`. However, by using the [`WithJsonSchema`][pydantic.json_schema.WithJsonSchema] +annotation, we can override the generated JSON schema for the custom `MyInt` type: + +```py output="json" +import json + +from typing_extensions import Annotated + +from pydantic import BaseModel, PlainValidator, WithJsonSchema + +MyInt = Annotated[ + int, + PlainValidator(lambda v: int(v) + 1), + WithJsonSchema({'type': 'integer', 'examples': [1, 0, -1]}), +] + + +class Model(BaseModel): + a: MyInt + + +print(Model(a='1').a) +#> 2 + +print(json.dumps(Model.model_json_schema(), indent=2)) +""" +{ + "properties": { + "a": { + "examples": [ + 1, + 0, + -1 + ], + "title": "A", + "type": "integer" + } + }, + "required": [ + "a" + ], + "title": "Model", + "type": "object" } """ ``` -!!! note - Defaults can be set outside `Annotated` as the assigned value or with `Field.default_factory` inside `Annotated`. - The `Field.default` argument is not supported inside `Annotated`. +!!! note: + As discussed in [this issue](https://github.com/pydantic/pydantic/issues/8208), in the future, it's likely that Pydantic will add + builtin support for JSON schema generation for types like [`PlainValidator`][pydantic.functional_validators.PlainValidator], + but the [`WithJsonSchema`][pydantic.json_schema.WithJsonSchema] annotation will still be useful for other custom types. + +### `SkipJsonSchema` annotation + +??? api "API Documentation" + [`pydantic.json_schema.SkipJsonSchema`][pydantic.json_schema.SkipJsonSchema]
-For versions of Python prior to 3.9, `typing_extensions.Annotated` can be used. +The [`SkipJsonSchema`][pydantic.json_schema.SkipJsonSchema] annotation can be used to skip a including field (or part of a field's specifications) +from the generated JSON schema. See the API docs for more details. -## Modifying the schema +### Implementing `__get_pydantic_core_schema__` Custom types (used as `field_name: TheType` or `field_name: Annotated[TheType, ...]`) as well as `Annotated` metadata (used as `field_name: Annotated[int, SomeMetadata]`) @@ -473,6 +845,9 @@ except ValidationError as e: """ ``` +!!! tip + Note that you *must* return a schema, even if you are just mutating it in place. + To override the schema completely, do not call the handler and return your own `CoreSchema`: @@ -572,6 +947,88 @@ print(m.model_fields) """ ``` +### Implementing `__get_pydantic_json_schema__` + +You can also implement `__get_pydantic_json_schema__` to modify or override the generated json schema. +Modifying this method only affects the JSON schema - it doesn't affect the core schema, which is used for validation and serialization. + +Here's an example of modifying the generated JSON schema: + +```py output="json" +import json +from typing import Any + +from pydantic_core import core_schema + +from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler, TypeAdapter +from pydantic.json_schema import JsonSchemaValue + + +class Person: + name: str + age: int + + def __init__(self, name: str, age: int): + self.name = name + self.age = age + + @classmethod + def __get_pydantic_core_schema__( + cls, source_type: Any, handler: GetCoreSchemaHandler + ) -> core_schema.CoreSchema: + return core_schema.typed_dict_schema( + { + 'name': core_schema.typed_dict_field(core_schema.str_schema()), + 'age': core_schema.typed_dict_field(core_schema.int_schema()), + }, + ) + + @classmethod + def __get_pydantic_json_schema__( + cls, core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler + ) -> JsonSchemaValue: + json_schema = handler(core_schema) + json_schema = handler.resolve_ref_schema(json_schema) + json_schema['examples'] = [ + { + 'name': 'John Doe', + 'age': 25, + } + ] + json_schema['title'] = 'Person' + return json_schema + + +print(json.dumps(TypeAdapter(Person).json_schema(), indent=2)) +""" +{ + "examples": [ + { + "age": 25, + "name": "John Doe" + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "age": { + "title": "Age", + "type": "integer" + } + }, + "required": [ + "name", + "age" + ], + "title": "Person", + "type": "object" +} +""" +``` + + ## JSON schema types Types, custom field types, and constraints (like `max_length`) are mapped to the corresponding spec formats in the @@ -586,6 +1043,7 @@ The field schema mapping from Python or Pydantic to JSON schema is done as follo {{ schema_mappings_table }} + ## Top-level schema generation You can also generate a top-level JSON schema that only includes a list of models and related @@ -659,132 +1117,7 @@ print(json.dumps(top_level_schema, indent=2)) """ ``` -## Schema customization - -You can customize the generated `$ref` JSON location: the definitions are always stored under the key -`$defs`, but a specified prefix can be used for the references. - -This is useful if you need to extend or modify the JSON schema default definitions location. For example, with OpenAPI: - -```py output="json" -import json - -from pydantic import BaseModel -from pydantic.type_adapter import TypeAdapter - - -class Foo(BaseModel): - a: int - - -class Model(BaseModel): - a: Foo - - -adapter = TypeAdapter(Model) - -print( - json.dumps( - adapter.json_schema(ref_template='#/components/schemas/{model}'), - indent=2, - ) -) -""" -{ - "$defs": { - "Foo": { - "properties": { - "a": { - "title": "A", - "type": "integer" - } - }, - "required": [ - "a" - ], - "title": "Foo", - "type": "object" - } - }, - "properties": { - "a": { - "$ref": "#/components/schemas/Foo" - } - }, - "required": [ - "a" - ], - "title": "Model", - "type": "object" -} -""" -``` - -It's also possible to extend or override the generated JSON schema in a model by implementing -`__get_pydantic_json_schema__` on your model. - -For example, you could add `examples` to the JSON schema: - -```py output="json" -import json - -from pydantic_core import CoreSchema - -from pydantic import BaseModel, GetJsonSchemaHandler -from pydantic.json_schema import JsonSchemaValue - - -class Person(BaseModel): - name: str - age: int - - @classmethod - def __get_pydantic_json_schema__( - cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler - ) -> JsonSchemaValue: - json_schema = handler(core_schema) - json_schema = handler.resolve_ref_schema(json_schema) - json_schema['examples'] = [ - { - 'name': 'John Doe', - 'age': 25, - } - ] - return json_schema - - -print(json.dumps(Person.model_json_schema(), indent=2)) -""" -{ - "examples": [ - { - "age": 25, - "name": "John Doe" - } - ], - "properties": { - "name": { - "title": "Name", - "type": "string" - }, - "age": { - "title": "Age", - "type": "integer" - } - }, - "required": [ - "name", - "age" - ], - "title": "Person", - "type": "object" -} -""" -``` - -Note that you *must* return a schema, even if you are just mutating it in place. - -## Customizing the JSON schema generation process +## Customizing the JSON Schema Generation Process ??? api "API Documentation" [`pydantic.json_schema`][pydantic.json_schema.GenerateJsonSchema]
@@ -870,3 +1203,79 @@ print(validation_schema) } """ ``` + +## Customizing the `$ref`s in JSON Schema + +The format of `$ref`s can be altered by calling [`model_json_schema()`][pydantic.main.BaseModel.model_json_schema] +or `model_dump_json()`][pydantic.main.BaseModel.model_dump_json] with the `ref_template` keyword argument. +The definitions are always stored under the key `$defs`, but a specified prefix can be used for the references. + +This is useful if you need to extend or modify the JSON schema default definitions location. For example, with OpenAPI: + +```py output="json" +import json + +from pydantic import BaseModel +from pydantic.type_adapter import TypeAdapter + + +class Foo(BaseModel): + a: int + + +class Model(BaseModel): + a: Foo + + +adapter = TypeAdapter(Model) + +print( + json.dumps( + adapter.json_schema(ref_template='#/components/schemas/{model}'), + indent=2, + ) +) +""" +{ + "$defs": { + "Foo": { + "properties": { + "a": { + "title": "A", + "type": "integer" + } + }, + "required": [ + "a" + ], + "title": "Foo", + "type": "object" + } + }, + "properties": { + "a": { + "$ref": "#/components/schemas/Foo" + } + }, + "required": [ + "a" + ], + "title": "Model", + "type": "object" +} +""" +``` + +## Miscellaneous Notes on JSON Schema Generation + +* The JSON schema for `Optional` fields indicates that the value `null` is allowed. +* The `Decimal` type is exposed in JSON schema (and serialized) as a string. +* Since the `namedtuple` type doesn't exist in JSON, a model's JSON schema does not preserve `namedtuple`s as `namedtuple`s. +* Sub-models used are added to the `$defs` JSON attribute and referenced, as per the spec. +* Sub-models with modifications (via the `Field` class) like a custom title, description, or default value, + are recursively included instead of referenced. +* The `description` for models is taken from either the docstring of the class or the argument `description` to + the `Field` class. +* The schema is generated by default using aliases as keys, but it can be generated using model + property names instead by calling [`model_json_schema()`][pydantic.main.BaseModel.model_json_schema] or + [`model_dump_json()`][pydantic.main.BaseModel.model_dump_json] with the `by_alias=False` keyword argument. diff --git a/pdm.lock b/pdm.lock index d37751aac0..9cfdb31fc9 100644 --- a/pdm.lock +++ b/pdm.lock @@ -3,8 +3,9 @@ [metadata] groups = ["default", "docs", "email", "linting", "memray", "mypy", "testing", "testing-extra"] -strategy = ["cross_platform"] -lock_version = "4.4" +cross_platform = true +static_urls = false +lock_version = "4.3" content_hash = "sha256:bfa326e0a46425e1cdaf0d38b8442a10a0a70771a0a7251918eb4b83fecb2298" [[package]] diff --git a/pydantic/fields.py b/pydantic/fields.py index c48e4c4c7d..263247b064 100644 --- a/pydantic/fields.py +++ b/pydantic/fields.py @@ -96,7 +96,7 @@ class FieldInfo(_repr.Representation): examples: List of examples of the field. exclude: Whether to exclude the field from the model serialization. discriminator: Field name or Discriminator for discriminating the type in a tagged union. - json_schema_extra: Dictionary of extra JSON schema properties. + json_schema_extra: A dict or callable to provide extra JSON schema properties. frozen: Whether the field is frozen. validate_default: Whether to validate the default value of the field. repr: Whether to include the field in representation of the model. @@ -719,7 +719,7 @@ def Field( # noqa: C901 examples: Example values for this field. exclude: Whether to exclude the field from the model serialization. discriminator: Field name or Discriminator for discriminating the type in a tagged union. - json_schema_extra: Any additional JSON schema data for the schema property. + json_schema_extra: A dict or callable to provide extra JSON schema properties. frozen: Whether the field is frozen. validate_default: Run validation that isn't only checking existence of defaults. This can be set to `True` or `False`. If not set, it defaults to `None`. repr: A boolean indicating whether to include the field in the `__repr__` output. @@ -954,7 +954,7 @@ class ComputedFieldInfo: title: Title of the computed field as in OpenAPI document, should be a short summary. description: Description of the computed field as in OpenAPI document. examples: Example values of the computed field as in OpenAPI document. - json_schema_extra: Dictionary of extra JSON schema properties. + json_schema_extra: A dict or callable to provide extra JSON schema properties. repr: A boolean indicating whether or not to include the field in the __repr__ output. """ @@ -1019,7 +1019,9 @@ def computed_field( repr: bool | None = None, return_type: Any = PydanticUndefined, ) -> PropertyT | typing.Callable[[PropertyT], PropertyT]: - """Decorator to include `property` and `cached_property` when serializing models or dataclasses. + """Usage docs: https://docs.pydantic.dev/2.6/concepts/fields#the-computed_field-decorator + + Decorator to include `property` and `cached_property` when serializing models or dataclasses. This is useful for fields that are computed from other fields, or for fields that are expensive to compute and should be cached. @@ -1143,7 +1145,7 @@ def _private_property(self) -> int: description: Description to use when including this computed field in JSON Schema, defaults to the function's docstring examples: Example values to use when including this computed field in JSON Schema - json_schema_extra: Dictionary of extra JSON schema properties. + json_schema_extra: A dict or callable to provide extra JSON schema properties. repr: whether to include this computed field in model repr. Default is `False` for private properties and `True` for public properties. return_type: optional return for serialization logic to expect when serializing to JSON, if included diff --git a/pydantic/json_schema.py b/pydantic/json_schema.py index 1b9b0d17d1..7839ed8cc7 100644 --- a/pydantic/json_schema.py +++ b/pydantic/json_schema.py @@ -2243,7 +2243,9 @@ def _sort_json_schema(value: JsonSchemaValue, parent_key: str | None = None) -> @dataclasses.dataclass(**_internal_dataclass.slots_true) class WithJsonSchema: - """Add this as an annotation on a field to override the (base) JSON schema that would be generated for that field. + """Usage docs: https://docs.pydantic.dev/2.6/concepts/json_schema/#withjsonschema-annotation + + Add this as an annotation on a field to override the (base) JSON schema that would be generated for that field. This provides a way to set a JSON schema for types that would otherwise raise errors when producing a JSON schema, such as Callable, or types that have an is-instance core schema, without needing to go so far as creating a custom subclass of pydantic.json_schema.GenerateJsonSchema. @@ -2330,20 +2332,53 @@ def _get_all_json_refs(item: Any) -> set[JsonRef]: @dataclasses.dataclass(**_internal_dataclass.slots_true) class SkipJsonSchema: - """Add this as an annotation on a field to skip generating a JSON schema for that field. + """Usage docs: https://docs.pydantic.dev/2.6/concepts/json_schema/#skipjsonschema-annotation + + Add this as an annotation on a field to skip generating a JSON schema for that field. Example: ```py + from typing import Union + from pydantic import BaseModel from pydantic.json_schema import SkipJsonSchema - class Model(BaseModel): - a: int | SkipJsonSchema[None] = None + from pprint import pprint - print(Model.model_json_schema()) - #> {'properties': {'a': {'default': None, 'title': 'A', 'type': 'integer'}}, 'title': 'Model', 'type': 'object'} + class Model(BaseModel): + a: Union[int, None] = None # (1)! + b: Union[int, SkipJsonSchema[None]] = None # (2)! + c: SkipJsonSchema[Union[int, None]] = None # (3)! + + + pprint(Model.model_json_schema()) + ''' + { + 'properties': { + 'a': { + 'anyOf': [ + {'type': 'integer'}, + {'type': 'null'} + ], + 'default': None, + 'title': 'A' + }, + 'b': { + 'default': None, + 'title': 'B', + 'type': 'integer' + } + }, + 'title': 'Model', + 'type': 'object' + } + ''' ``` + + 1. The integer and null types are both included in the schema for `a`. + 2. The integer type is the only type included in the schema for `b`. + 3. The entirety of the `c` field is omitted from the schema. """ def __class_getitem__(cls, item: AnyType) -> AnyType: