Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

naming of originating model in model_json_schema with pydantic_queryset_creator #1728

Open
markus-96 opened this issue Oct 7, 2024 · 0 comments
Labels
pydantic Issue related to tortoise.contrib.pydantic

Comments

@markus-96
Copy link
Contributor

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.

@henadzit henadzit added the pydantic Issue related to tortoise.contrib.pydantic label Nov 28, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pydantic Issue related to tortoise.contrib.pydantic
Projects
None yet
Development

No branches or pull requests

2 participants