Reputation: 71
I am working with Pydantic models in FastAPI, and I have a structure where RCSMessage
can be one of several possible models. I am using Union
to allow multiple message types and extra = "forbid"
to prevent unknown properties. However, when I send a JSON with a property that does not exist in any model, Pydantic does not raise an error as expected.
from pydantic import BaseModel, Field
from typing import Optional, Union
from datetime import datetime
class MessageContact(BaseModel):
userContact: Optional[str] = Field(
None, pattern=r"^\+\d{1,15}$"
) # MSISDN E.164 format
chatId: Optional[str] = None # Example UUID
class RCSMessageBase(BaseModel):
msgId: Optional[str] = None
status: Optional[str] = None
trafficType: Optional[str] = None
expiry: Optional[datetime] = None # ISO 8601 date-time format
timestamp: Optional[datetime] = None # ISO 8601 date-time format
class Config:
extra = "forbid"
class SuggestedResponse(BaseModel):
suggestedResponse: Optional[dict] = None
class Config:
extra = "forbid"
class SharedData(BaseModel):
sharedData: Optional[dict] = None
class Config:
extra = "forbid"
class IsTyping(BaseModel):
isTyping: Optional[str] = Field(None, pattern="^(active|idle)$")
class Config:
extra = "forbid"
RCSMessageType = Union[
SuggestedResponse, SharedData, IsTyping
]
RCSMessage = Union[RCSMessageBase, RCSMessageType]
class RCSMessageWithContactInfo(BaseModel):
RCSMessage: RCSMessage
messageContact: MessageContact
When I send the following request:
{
"RCSMessage": {
"isTypingNoExiste": "active"
},
"messageContact": {
"userContact": "+14251234567"
}
}
I expect Pydantic to raise an error because isTypingNoExiste
is not a valid property in IsTyping
. However, the validation does not fail, and the JSON is accepted.
How can I make Pydantic reject any unknown properties within RCSMessage
, ensuring that only the defined properties in the models within the Union
are allowed?
Upvotes: 0
Views: 19
Reputation: 1
The issue lies in how Pydantic applies model configurations to subclasses of BaseModel.
Since you haven't applied the forbid option in the IsTyping class, a model configured with all defaults (extra=ignore) became a valid option.
Configurations are inherited, so if you want to change such behaviour for multiple models, it is advised to subclass BaseModel:
class MessageContact(BaseModel):
userContact: Optional[str] = Field(
None, pattern=r"^\+\d{1,15}$"
) # MSISDN E.164 format
chatId: Optional[str] = None # Example UUID
class StrictBaseModel(BaseModel):
model_config = ConfigDict(extra="forbid")
class RCSMessageBase(StrictBaseModel):
msgId: Optional[str] = None
status: Optional[str] = None
trafficType: Optional[str] = None
expiry: Optional[datetime] = None # ISO 8601 date-time format
timestamp: Optional[datetime] = None # ISO 8601 date-time format
class SuggestedResponse(StrictBaseModel):
suggestedResponse: Optional[dict] = None
class SharedData(StrictBaseModel):
sharedData: Optional[dict] = None
class IsTyping(StrictBaseModel):
isTyping: Optional[str] = Field(None, pattern="^(active|idle)$")
RCSMessageType = Union[
SuggestedResponse, SharedData, IsTyping
]
RCSMessage = Union[RCSMessageBase, RCSMessageType]
class RCSMessageWithContactInfo(BaseModel):
RCSMessage: RCSMessage
messageContact: MessageContact
if __name__ == "__main__":
print( RCSMessageWithContactInfo.model_validate({
"RCSMessage": {
"isTypingNoExiste": "active"
},
"messageContact": {"userContact": "+14251234567"}}))
Upvotes: 0