naming of originating model in model_json_schema with pydantic_queryset_creator
markus-96 opened this issue · 0 comments
Describe the bug
If I use pydantic_queryset_creator
, I get a Pydantic RootModel
of type List
. Normally, if I call model_json_schema
of a RootModel
, the originating model name is referenced in $defs. But if I use pydantic_queryset_creator
, the model name is always leaf
in the produced json schema. Additionaly, if I specify a name when calling pydantic_queryset_creator
, this name will be used for the type of object that is in the array and also for the array itself, that makes no sense for me. It is a bit hard to explain for me, so I provided some example code with additional comments that I hope will be enough to understand what I meen. If not, please ask!
I also rewrote pydantic_queryset_creator
to match my desired behaviour, but maybe I completely get the function wrong. Maybe it also breaks something with pydantic_model_creator, that is for to complex for me to fully understand.
Example code with additional comments
import json
from typing import Type, Optional, List
from pydantic import BaseModel, RootModel, create_model, Field
from tortoise import Model
from tortoise.contrib.pydantic import pydantic_model_creator, pydantic_queryset_creator, PydanticListModel
from tortoise.contrib.pydantic.creator import _cleandoc
from tortoise.fields import IntField
this is for explaining what I was expecting from pydantic_queryset_creator
:
class Pet(BaseModel): # <-- normal BaseModel
id: int
class Pets(RootModel[list[Pet]]): # <-- normal RootModel
...
# RootModel, like it is generated in pydantic_queryset_creator
PetsGenerated = create_model(f"{Pet.__name__}_list", __base__=RootModel, root=(list[Pet], Field(default_factory=list)))
and how pydantic_queryset_creator
actually behaves:
class PetTortoise(Model): # <-- basic Tortoise Model
id = IntField(pk=True)
...
PetTortoisePydantic = pydantic_model_creator(PetTortoise) # <-- this works fine
PetTortoiseListPydantic = pydantic_queryset_creator(PetTortoise) # <-- array with "leafs" as items
PetTortoiseListPydanticWithName = pydantic_queryset_creator(PetTortoise, name=PetTortoise.__name__) # <-- array with "PetTortoise" as title and "PetTortoise" as items
and my slightly modified version of pydantic_queryset_creator
:
def pydantic_queryset_creator_patched(
cls: "Type[Model]",
*,
name=None,
submodel_name=None, # <-- introduced option to explicitly name the items of the array
exclude: tuple[str, ...] = (),
include: tuple[str, ...] = (),
computed: tuple[str, ...] = (),
allow_cycles: Optional[bool] = None,
sort_alphabetically: Optional[bool] = None,
) -> Type[PydanticListModel]:
_submodel_name = submodel_name or cls.__name__ # <-- if not set, use the name of the originating model as name for the submodel
submodel = pydantic_model_creator(
cls,
exclude=exclude,
include=include,
computed=computed,
allow_cycles=allow_cycles,
sort_alphabetically=sort_alphabetically,
name=_submodel_name,
)
lname = name or f"{cls.__name__}_list"
# Creating Pydantic class for the properties generated before
model = create_model(
lname,
__base__=PydanticListModel,
root=(List[submodel], Field(default_factory=list)), # type: ignore
)
# Copy the Model docstring over
model.__doc__ = _cleandoc(cls)
# The title of the model to hide the hash postfix
model.model_config["title"] = name or f"{submodel.model_config['title']}_list"
model.model_config["submodel"] = submodel # type: ignore
return model
PetTortoiseListPydanticPatched = pydantic_queryset_creator_patched(PetTortoise) # <-- array named "PetTortoise_list" with items "PetTortoise"
if __name__ == "__main__":
print(json.dumps(Pet.model_json_schema(), indent=2))
"""results in:
{
"properties": {
"id": {
"title": "Id",
"type": "integer"
}
},
"required": [
"id"
],
"title": "Pet", # <-- title is Pet
"type": "object"
}
"""
a manually created RootModel, where the title is passed in via RootModel.__name__
print(json.dumps(Pets.model_json_schema(), indent=2))
"""results in:
{
"$defs": {
"Pet": { # <-- the items are of type Pet
"properties": {
"id": {
"title": "Id",
"type": "integer"
}
},
"required": [
"id"
],
"title": "Pet",
"type": "object"
}
},
"items": {
"$ref": "#/$defs/Pet"
},
"title": "Pets", # <-- we are dealing with Pets here
"type": "array"
}
"""
If we generate a RootModel like in pydantic_queryset_creator
, it works perfectly fine:
print(json.dumps(PetsGenerated.model_json_schema(), indent=2))
"""results in:
{
"$defs": {
"Pet": {
"properties": {
"id": {
"title": "Id",
"type": "integer"
}
},
"required": [
"id"
],
"title": "Pet",
"type": "object"
}
},
"items": {
"$ref": "#/$defs/Pet"
},
"title": "Pet_list", # <-- generated title like in pydantic_queryset_creator
"type": "array"
}
"""
pydantic_model_creator
works perfectly fine:
print(json.dumps(PetTortoisePydantic.model_json_schema(), indent=2))
"""results in:
{
"additionalProperties": false,
"properties": {
"id": {
"maximum": 2147483647,
"minimum": -2147483648,
"title": "Id",
"type": "integer"
}
},
"required": [
"id"
],
"title": "PetTortoise", # <-- title is correct
"type": "object"
}
"""
what is leaf?
print(json.dumps(PetTortoiseListPydantic.model_json_schema(), indent=2))
"""results in:
{
"$defs": {
"leaf": { # <-- what is leaf?
"additionalProperties": false,
"properties": {
"id": {
"maximum": 2147483647,
"minimum": -2147483648,
"title": "Id",
"type": "integer"
}
},
"required": [
"id"
],
"title": "PetTortoise", # <-- this is correct
"type": "object"
}
},
"items": {
"$ref": "#/$defs/leaf"
},
"title": "PetTortoise_list",
"type": "array"
}
"""
title
of items and array are the same:
print(json.dumps(PetTortoiseListPydanticWithName.model_json_schema(), indent=2))
"""results in:
{
"$defs": {
"PetTortoise": { # <-- this is perfect
"additionalProperties": false,
"properties": {
"id": {
"maximum": 2147483647,
"minimum": -2147483648,
"title": "Id",
"type": "integer"
}
},
"required": [
"id"
],
"title": "PetTortoise", # <-- also this
"type": "object"
}
},
"items": {
"$ref": "#/$defs/PetTortoise"
},
"title": "PetTortoise", # <-- why is this the same as the items?
"type": "array"
}
"""
desired behaviour:
print(json.dumps(PetTortoiseListPydanticPatched.model_json_schema(), indent=2))
"""results in:
{
"$defs": {
"PetTortoise": { # <-- :)
"additionalProperties": false,
"properties": {
"id": {
"maximum": 2147483647,
"minimum": -2147483648,
"title": "Id",
"type": "integer"
}
},
"required": [
"id"
],
"title": "PetTortoise", # <-- :)
"type": "object"
}
},
"items": {
"$ref": "#/$defs/PetTortoise"
},
"title": "PetTortoise_list", # <-- :)
"type": "array"
}
"""
Additional context
I want to dynamically build OpenAPI Spec with the definitions provided by model_json_schema of the Pydantic BaseModels generated by pydantic_model_creator and pydantic_queryset_creator. For that, I need to remove all $defs that are referenced in a $ref and store them in #/components/schemas for example. Patching every $ref to point to #/components/schemas can be easily done by providing a ref_template for model_json_schema. But it is not great that every $def of a pydantic list model has "leaf" as a key.