AlJw
AlJw

Reputation: 71

Pydantic Validation Issue with Union Models and extra = "forbid"

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.

Pydantic Models

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

Issue

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.

Question

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

Answers (1)

Erik Horn
Erik Horn

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

Related Questions