For example I have the following toy example of a Parent
Model:
from pydantic import BaseModel, Extra
class Parent(BaseModel):
class Config:
extra = Extra.ignore
validate_assignment = True
schema_extra = {
"version": "00.00.00",
"info": "Parent description",
"name": "Parent Name",
}
The goal here is that all child models inherit the same schema and maybe add additional stuff to that schema.
class Child(Parent):
class Config:
extra = Extra.ignore
validate_assignment = True
@staticmethod
def schema_extra(
schema: dict[str, object], model: type["Child"]
) -> None:
schema['info'] = 'Child Description'
schema['name'] = 'Child Name'
schema['additional stuff'] = 'Something else'
The code above does not work, since the Config
in the child class completely overwrites the inherited Config
from the parent, therefore in the child's schema we are missing the version
for example.
As mentioned, I want for all inherited classes to have the same basic schema layout with some meta information and possibly change values or add to it. Is this even possible?
Edit The solution from @Daniil-Fajnberg works well with one caveat. Having for example:
class Child(Parent):
class Config:
@staticmethod
def schema_extra(schema: dict[str, object]) -> None:
schema["info"] = "Child Description"
schema["additional stuff"] = "Something else"
schema["name"] = f"{schema.get('title')}v{schema.get('version')}"
the resulting entry for name
in the schema will be for example:
'name': "Nonev00.00.00"
FYI: In my setup I use schema_extra
as static method on the parent as well
First of all, this statement is not entirely correct:
the
Config
in the child class completely overwrites the inheritedConfig
from the parent
The Config
itself is inherited. But individual Config
attributes are overridden.
Example:
from pydantic import BaseModel, Extra
class Parent(BaseModel):
class Config:
extra = Extra.allow
validate_assignment = True
class Child(Parent):
class Config:
extra = Extra.forbid
print(Child.__config__.extra) # Extra.forbid
print(Child.__config__.validate_assignment) # True
That being said, the problem you are facing is that you are overriding the schema_extra
attribute in your Child.Config
. In other words Child.Config.schema_extra
does replace Parent.Config.schema_extra
.
If we wish to make this specific attribute of the configuration extensible by child models rather than have them override it, we have to get a bit creative.
With newer Pydantic versions this has become quite easier. In the baseclass instead of setting schema extra, one can simply use __get_pydantic_json_schema__
to get the current schema and modify it.
from pydantic import BaseModel, GetJsonSchemaHandler, Field
from pydantic.json_schema import JsonSchemaValue
from pydantic_core import core_schema
def schema_extra(schema: dict[str, Any], model: type["Parent"]) -> None:
schema["version"] = "1"
schema["info"] = "Parent description"
class Parent(BaseModel, json_schema_extra=schema_extra):
some_parent_property: str
class Child(Parent):
@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 ["info"] = "Child Description"
json_schema ["additional stuff"] = "Something else"
return json_schema
print(Child.schema_json(indent=4))
I propose defining a custom BaseModel
to use throughout the application, hooking into its __init_subclass__
method and applying a custom function to "inherit" the schema_extra
config attribute from all ancestor models (in reverse MRO) iteratively.
To ensure that any schema_extra
function calls are deferred until the schema dictionary is actually constructed, we will need to ensure that every subclass will have its schema_extra
config value as defined by the user saved in a separate config attribute. This is different from my initial solution (see below), which was sub-optimal because it called the functions immediately during class creation and thus would not have access to some schema attributes/keys that are only created later.
This seems to work:
from collections.abc import Iterable
from functools import partial
from inspect import signature
from typing import Any, ClassVar, Union
from pydantic import BaseModel as PydanticBaseModel
from pydantic.config import BaseConfig as PydanticBaseConfig, SchemaExtraCallable
class BaseConfig(PydanticBaseConfig):
own_schema_extra: Union[dict[str, Any], SchemaExtraCallable] = {}
class BaseModel(PydanticBaseModel):
__config__: ClassVar[type[BaseConfig]] = BaseConfig
@classmethod
def __init_subclass__(cls, **kwargs: object) -> None:
cls.__config__.own_schema_extra = cls.__config__.schema_extra
cls.__config__.schema_extra = partial(
inherited_schema_extra,
base_classes=(
base for base in reversed(cls.__mro__)
if issubclass(base, BaseModel)
),
)
def inherited_schema_extra(
schema: dict[str, Any],
model: type[BaseModel],
*,
base_classes: Iterable[type[BaseModel]],
) -> None:
for base in base_classes:
base_schema_extra = base.__config__.own_schema_extra
if callable(base_schema_extra):
if len(signature(base_schema_extra).parameters) == 1:
base_schema_extra(schema)
else:
base_schema_extra(schema, model)
else:
schema.update(base_schema_extra)
Note that all this business with the BaseConfig
annotation is mostly just to improve type safety and define our new own_schema_extra
config attribute properly before using it. It is not strictly necessary to do this, but it makes this a cleaner solution.
class Parent1(BaseModel):
class Config:
schema_extra = {
"version": "1",
"info": "Parent1 description",
}
class Parent2(BaseModel):
class Config:
schema_extra = {
"version": "2",
"info": "Parent2 description",
}
class Child(Parent2, Parent1):
class Config:
@staticmethod
def schema_extra(schema: dict[str, Any]) -> None:
schema["info"] = "Child Description"
schema["additional stuff"] = "Something else"
schema["title+version"] = f'{schema["title"]}v{schema["version"]}'
print(Child.schema_json(indent=4))
{
"title": "Child",
"type": "object",
"properties": {},
"version": "2",
"info": "Child Description",
"additional stuff": "Something else",
"title+version": "Childv2"
}
As you can see here, multiple inheritance for the schema is supported properly (because we follow the MRO to construct it) and the deferred call to the schema_extra
functions allowed us to access the title
key of the schema in one of them to construct the title+version
value.
I propose hooking into __init_subclass__
and applying a custom function to "inherit" the schema_extra
config attribute from all parent models (in reverse MRO) iteratively:
from __future__ import annotations
from inspect import signature
from pydantic import BaseModel as PydanticBaseModel
def inherit_model_schema_extra(model: type[BaseModel]) -> None:
schema_extra: dict[str, object] = {}
for parent in reversed(model.__mro__):
if not issubclass(parent, BaseModel):
continue
parent_schema_extra = parent.__config__.schema_extra
if callable(parent_schema_extra):
if len(signature(parent_schema_extra).parameters) == 1:
parent_schema_extra(schema_extra)
else:
parent_schema_extra(schema_extra, model)
else:
schema_extra.update(parent_schema_extra)
model.__config__.schema_extra = schema_extra
class BaseModel(PydanticBaseModel):
@classmethod
def __init_subclass__(cls, **kwargs: object) -> None:
inherit_model_schema_extra(cls)
The way schema_extra
is handled here (i.e. as a callable or as a dictionary) is essentially copied from how the model_process_schema
function in the current stable version does it. (see line 591 and following)
class Parent(BaseModel):
class Config:
schema_extra = {
"version": "00.00.00",
"info": "Parent description",
}
class Child(Parent):
class Config:
@staticmethod
def schema_extra(schema: dict[str, object]) -> None:
schema["info"] = "Child Description"
schema["additional stuff"] = "Something else"
print(Child.schema_json(indent=4))
{
"title": "Child",
"type": "object",
"properties": {},
"version": "00.00.00",
"info": "Child Description",
"additional stuff": "Something else"
}
As you can see, the Child
schema has both the Parent
extras as well as its own, whereby its own extras take precedence.