pythonmongodbparsingobjectidpydantic

How to parse ObjectId in a pydantic model?


I am trying to parse MongoDB records to a pydantic model but failing to do so for ObjectId

From what I understood, I need to setup validator for ObjectId and did try to both extend ObjectId class and add the validator decorator to my class using ObjectId. which I did as follows.

from pydantic import BaseModel, validator
from bson.objectid import ObjectId


class ObjectId(ObjectId):
    pass
    @classmethod
    def __get_validators__(cls):
        yield cls.validate
    @classmethod
    def validate(cls, v):
        if not isinstance(v, ObjectId):
            raise TypeError('ObjectId required')
        return str(v)


class User(BaseModel):
    who: ObjectId


class User1(BaseModel):
    who: ObjectId
    @validator('who')
    def validate(cls, v):
        if not isinstance(v, ObjectId):
            raise TypeError('ObjectId required')
        return str(v)

data = {"who":ObjectId('123456781234567812345678')}

Unfortunately, both "solution" are failing as follows:

>>> test = User(**data)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "pydantic/main.py", line 274, in pydantic.main.BaseModel.__init__
pydantic.error_wrappers.ValidationError: 1 validation error for User
id
  field required (type=value_error.missing)
>>> test = User1(**data)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "pydantic/main.py", line 274, in pydantic.main.BaseModel.__init__
pydantic.error_wrappers.ValidationError: 1 validation error for User1
who
  ObjectId required (type=type_error)

There is definitely something that I am missing here.


Solution

  • Pydantic 1

    You first test case works fine. The problem is with how you overwrite ObjectId.

    from pydantic import BaseModel
    from bson.objectid import ObjectId as BsonObjectId
    
    
    class PydanticObjectId(BsonObjectId):
        @classmethod
        def __get_validators__(cls):
            yield cls.validate
    
        @classmethod
        def validate(cls, v):
            if not isinstance(v, BsonObjectId):
                raise TypeError('ObjectId required')
            return str(v)
    
    
    class User(BaseModel):
        who: PydanticObjectId
    
    
    print(User(who=BsonObjectId('123456781234567812345678')))
    
    

    prints

    who='123456781234567812345678'
    

    Only pydantic should use pydantic type. Mongo will provide you with bsons ObjectId. So instantiate your data with real ObjectId. So data = {"who":ObjectId('123456781234567812345678')} is wrong, as it uses your child ObjectId class.

    Pydantic 2

    Use AfterValidator https://docs.pydantic.dev/latest/usage/validators/

    from typing_extensions import Annotated
    from pydantic import BaseModel
    from pydantic.functional_validators import AfterValidator
    from bson import ObjectId as _ObjectId
    
    
    def check_object_id(value: str) -> str:
        if not _ObjectId.is_valid(value):
            raise ValueError('Invalid ObjectId')
        return value
    
    
    ObjectId = Annotated[str, AfterValidator(check_object_id)]
    
    
    class Example(BaseModel):
        id: ObjectId
    
    
    print(Example(id='5f9b3b3b9d9f3d0001a3b3b3'))
    print(Example(id='1'))