Search code examples
pythonfastapipydantic

How to hide a Pydantic discriminator field from FastAPI docs


We have a discriminator field type which we want to hide from the Swagger UI docs:

class Foo(BDCBaseModel):
    type: Literal["Foo"] = Field("Foo", exclude=True)
    Name: str

class Bar(BDCBaseModel):
    type: Literal["Bar"] = Field("Bar", exclude=True)
    Name: str

class Demo(BDCBaseModel):
    example: Union[Foo, Bar] = Field(discriminator="type")

The following router:

@router.post("/demo")
async def demo(
    foo: Foo,
):
    demo = Demo(example=foo)
    return demo

And this is shown in the Swagger docs: enter image description here

We don't want the user to see the type field as it is useless for him/her anyways. We tried making the field private: _type which hides it from the docs but then it cannot be used as discriminator anymore:

    class Demo(BDCBaseModel):
  File "pydantic\main.py", line 205, in pydantic.main.ModelMetaclass.__new__
  File "pydantic\fields.py", line 491, in pydantic.fields.ModelField.infer
  File "pydantic\fields.py", line 421, in pydantic.fields.ModelField.__init__
  File "pydantic\fields.py", line 537, in pydantic.fields.ModelField.prepare
  File "pydantic\fields.py", line 639, in pydantic.fields.ModelField._type_analysis
  File "pydantic\fields.py", line 753, in pydantic.fields.ModelField.prepare_discriminated_union_sub_fields
  File "pydantic\utils.py", line 739, in pydantic.utils.get_discriminator_alias_and_values
pydantic.errors.ConfigError: Model 'Foo' needs a discriminator field for key '_type'

Solution

  • This is a very common situation and the solution is farily simple. Factor out that type field into its own separate model.

    The typical way to go about this is to create one FooBase with all the fields, validators etc. that all child models will share (in this example only name) and then subclass it as needed. In this example you would create one Foo subclass with that type field that you then use for the Demo annotation, and one FooRequest class without any additions.

    Here is a full working example:

    from typing import Literal, Union
    
    from fastapi import FastAPI
    from pydantic import BaseModel, Field
    
    class FooBase(BaseModel):
        name: str
    
    class FooRequest(FooBase):
        pass  # possibly configure other request specific things here
    
    class Foo(FooBase):
        type: Literal["Foo"] = Field("Foo", exclude=True)
    
        class Config:
            orm_mode = True
    
    class Bar(BaseModel):
        type: Literal["Bar"] = Field("Bar", exclude=True)
        name: str
    
    class Demo(BaseModel):
        example: Union[Foo, Bar] = Field(discriminator="type")
    
    api = FastAPI()
    
    @api.post("/demo")
    async def demo(foo: FooRequest):
        foo = Foo.from_orm(foo)
        return Demo(example=foo)
    

    Note that I used the orm_mode = True setting just to have a very concise way of converting a FooRequest instance into a Foo instance inside the route handler function. This is not necessary. You could also just do foo = Foo.parse_obj(foo.dict()) there.

    Also, the addition of the FooRequest model is redundant here of course. You can just as well use the FooBase as the request model. I wrote it this way just to demonstrate a typical pattern because sometimes the request model has additional things that distinguish it from its siblings. In your example it is overkill.