tortoise / tortoise/tortoise-orm
naming of originating model in model_json_schema with pydantic_queryset_creator
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 5.6k
- Forks
- 516
- Avg merge
- 2d 21h
- Merged PRs (30d)
- 9
Description
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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with pydantic_queryset_creator and pydantic_model_creator in tortoise.contrib.pydantic.creator, then compare their generated model_json_schema output with the examples in the issue. Trace how the list model and its item submodel are named; done means the array keeps its list title while the referenced item definition uses the originating model name instead of "leaf".
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- database
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100