Pydantic: What is the best way to represent shared fields in Pydantic?

Created on 19 Nov 2019  路  4Comments  路  Source: samuelcolvin/pydantic

Question

I quite frequently run into situations where I want to reuse the same field in different models, sharing attributes about them.

What is the recommended approach to do this? I wonder if this might be worth adding something to the documentation about?

I can think of two ways:

Approach One - Shared a Field object between models

I think this is probably the worse choice because it doesn't introduce a #ref for the shared field in the schema, but inlines it. However, it does seem slightly more natural in Pydantic.

from pydantic import BaseModel, Field

sharedField = Field("my-default", example="hello I am an example", description="An example field.", nullable=True)

class ModelOne(BaseModel):
    """
    Some documentation about ModelOne
    """
    myField:str = sharedField

class ModelTwo(BaseModel):
    """
    Some documentation about ModelTwo
    """
    myField:str = sharedField

Approach 2 - Create a separate model and assign a different __root__ type

from pydantic import BaseModel, Field

class SharedField(BaseModel):
    """
    Some documentation about SharedField
    """
    __root__: str
    class Config:
        schema_extra = {
            "example": "example of SharedField",
            "nullable": True
        }

class ModelOne(BaseModel):
    """
    Some documentation about ModelOne
    """
    myField:SharedField

class ModelTwo(BaseModel):
    """
    Some documentation about ModelTwo
    """
    myField:SharedField

Background

I currently have an API schema defined using Python dictionaries. I wrote some code to turn those dictionaries into OpenAPI and JSONSchema compatible specs.

I'm trying to replace some of my code with Pydantic, since I prefer the approach you have taken, and also because it seems quite polished already. Thanks for making it.

Requested Information

  • OS: Linux (Nixos 19.09)
  • Python version import sys; print(sys.version): 3.7.5
  • Pydantic version import pydantic; print(pydantic.VERSION): 1.0
question

All 4 comments

There are two other options:

  1. Make a custom type. Depending on what you want to do, this may be easier to work with and more performant than using a __root__ model.

  2. Inheritance:

class ModelOne(BaseModel):
    """
    Some documentation about ModelOne
    """
    myField:str = sharedField

class ModelTwo(ModelOne):
    """
    Some documentation about ModelTwo
    """
    # myField already present due to inheritance
    myOtherField: str

You can also use a mixin class if that better reflects your data hierarchy:

class MyFieldMixin(BaseModel):
    myField: str = sharedField  # defined in your example


class ModelOne(MyFieldMixin):
    """
    Some documentation about ModelOne
    """

class ModelTwo(MyFieldMixin):
    """
    Some documentation about ModelTwo
    """

I'd agree with @dmontagu that inheritance was probably the best way.

Let me know if somehow that doesn't work.

Thanks to both of you for your advice.

I will try out the mixin approach and see if that works.

What about more complex types? __root__ doesn't seem to support Field.
This example produces error for me:

class User(BaseModel):
    nickname: Name

class Name(BaseModel):
    __root__: str = Field(..., regex=r".+")

# AssertionError: Path params must be of one of the supported types

I cant use inheritance because I want to use Name type separately in a few places in fastapi.
Am I doing something wrong here?

NVM, the error was coming from fastapi, I had to replace nickaname = Name with nickname: str = Name

So with the help of https://pydantic-docs.helpmanual.io/usage/types/#classes-with-__get_validators__
I came up with something like this, not sure if this is an optimal solution:

class _RegexName(str):
    @classmethod
    def _get_pattern(cls) -> re.Pattern:
        raise NotImplementedError

    @classmethod
    def __get_validators__(cls):
        yield cls.validate

    @classmethod
    def validate(cls, v: Any) -> _RegexName:
        if not isinstance(v, str):
            raise TypeError("string required")

        if not cls._get_pattern().fullmatch(v):
            raise ValueError(f"invalid {cls.__name__} format")

        return cls(v)


class Name(_RegexName):
    @classmethod
    def _get_pattern(cls) -> re.Pattern:
        return _NAME_REGEX

    @classmethod
    def __modify_schema__(cls, field_schema):
        field_schema.update(
            pattern=_NAME_REGEX.pattern,
            examples=["my-name", "hello123"],
        )
Was this page helpful?
0 / 5 - 0 ratings

Related issues

iwoloschin picture iwoloschin  路  3Comments

ashpreetbedi picture ashpreetbedi  路  3Comments

ashears picture ashears  路  3Comments

sbv-trueenergy picture sbv-trueenergy  路  3Comments

cdeil picture cdeil  路  3Comments