I'm trying to convert UUID field into string when calling .dict() to save to a monogdb using pymongo. I tried with .json() but seems like mongodb doesn't like it
TypeError: document must be an instance of dict, bson.son.SON, bson.raw_bson.RawBSONDocument, or a type that inherits from collections.MutableMapping
Here is what I have done so far:
from uuid import uuid4
from datetime import datetime
from pydantic import BaseModel, Field, UUID4
class TestModel(BaseModel):
id: UUID4 = Field(default_factory=uuid4)
title: str = Field(default="")
ts: datetime = Field(default_factory=datetime.utcnow)
record = TestModel()
record.title = "Hello!"
print(record.json())
# {"id": "4d52517a-88a0-43f8-9d9a-df9d7b6ddf01", "title": "Hello!", "ts": "2021-08-18T03:00:54.913345"}
print(record.dict())
# {'id': UUID('4d52517a-88a0-43f8-9d9a-df9d7b6ddf01'), 'title': 'Hello!', 'ts': datetime.datetime(2021, 8, 18, 3, 0, 54, 913345)}
Any advice?
The best I can do is make a new method called to_dict() inside that model and call it instead
class TestModel(BaseModel):
id: UUID4 = Field(default_factory=uuid4)
title: str = Field(default="")
def to_dict(self):
data = self.dict()
data["id"] = self.id.hex
return data
record = TestModel()
print(record.to_dict())
# {'id': '03c088da40e84ee7aa380fac82a839d6', 'title': ''}
Pydantic has a possibility to transform or validate fields after the validation or at the same time. In that case, you need to use validator.
First way (this way validates/transforms at the same time to other fields):
from uuid import UUID, uuid4
from pydantic import BaseModel, validator, Field
class ExampleSerializer(BaseModel):
uuid: UUID = Field(default_factory=uuid4)
other_uuid: UUID = Field(default_factory=uuid4)
other_field: str
_transform_uuids = validator("uuid", "other_uuid", allow_reuse=True)(
lambda x: str(x) if x else x
)
req = ExampleSerializer(
uuid="a1fd6286-196c-4922-adeb-d48074f06d80",
other_uuid="a1fd6286-196c-4922-adeb-d48074f06d80",
other_field="123"
).dict()
print(req)
Second way (this way validates/transforms after the others):
from uuid import UUID, uuid4
from pydantic import BaseModel, validator, Field
class ExampleSerializer(BaseModel):
uuid: UUID = Field(default_factory=uuid4)
other_uuid: UUID = Field(default_factory=uuid4)
other_field: str
@validator("uuid", "other_uuid")
def validate_uuids(cls, value):
if value:
return str(value)
return value
req = ExampleSerializer(
uuid="a1fd6286-196c-4922-adeb-d48074f06d80",
other_uuid="a1fd6286-196c-4922-adeb-d48074f06d80",
other_field="123"
).dict()
print(req)
Result:
{'uuid': 'a1fd6286-196c-4922-adeb-d48074f06d80', 'other_uuid': 'a1fd6286-196c-4922-adeb-d48074f06d80', 'other_field': '123'}
Following on Pydantic's docs for classes-with-get_validators
I created the following custom type NewUuid.
It accepts a string matching the UUID format and validates it by consuming the value with uuid.UUID(). If the value is invalid, uuid.UUID() throws an exception (see example output) and if it's valid, then NewUuid returns a string (see example output). The exception is any of uuid.UUID()'s exceptions, but it's wrapped with Pydantic's exception as well.
The script below can run as is.
import uuid
from pydantic import BaseModel
class NewUuid(str):
"""
Partial UK postcode validation. Note: this is just an example, and is not
intended for use in production; in particular this does NOT guarantee
a postcode exists, just that it has a valid format.
"""
@classmethod
def __get_validators__(cls):
# one or more validators may be yielded which will be called in the
# order to validate the input, each validator will receive as an input
# the value returned from the previous validator
yield cls.validate
@classmethod
def __modify_schema__(cls, field_schema):
# __modify_schema__ should mutate the dict it receives in place,
# the returned value will be ignored
field_schema.update(
# simplified regex here for brevity, see the wikipedia link above
pattern='^[A-F0-9a-f]{8}(-[A-F0-9a-f]{4}){3}-[A-F0-9a-f]{12}$',
# some example postcodes
examples=['4a33135d-8aa3-47ba-bcfd-faa297b7fb5b'],
)
@classmethod
def validate(cls, v):
if not isinstance(v, str):
raise TypeError('string required')
u = uuid.UUID(v)
# you could also return a string here which would mean model.post_code
# would be a string, pydantic won't care but you could end up with some
# confusion since the value's type won't match the type annotation
# exactly
return cls(f'{v}')
def __repr__(self):
return f'NewUuid({super().__repr__()})'
class Resource(BaseModel):
id: NewUuid
name: str
print('-' * 20)
resource_correct_id: Resource = Resource(id='e8991fd8-b655-45ff-996f-8bc1f60f31e0', name='Server2')
print(resource_correct_id)
print(resource_correct_id.id)
print(resource_correct_id.dict())
print('-' * 20)
resource_malformed_id: Resource = Resource(id='X8991fd8-b655-45ff-996f-8bc1f60f31e0', name='Server3')
print(resource_malformed_id)
print(resource_malformed_id.id)
Example Output
--------------------
id=NewUuid('e8991fd8-b655-45ff-996f-8bc1f60f31e0') name='Server2'
e8991fd8-b655-45ff-996f-8bc1f60f31e0
{'id': NewUuid('e8991fd8-b655-45ff-996f-8bc1f60f31e0'), 'name': 'Server2'}
--------------------
Traceback (most recent call last):
File "/Users/smoshkovits/ws/fallback/playground/test_pydantic8_uuid.py", line 58, in <module>
resource_malformed_id: Resource = Resource(id='X8991fd8-b655-45ff-996f-8bc1f60f31e0', name='Server3')
File "pydantic/main.py", line 406, in pydantic.main.BaseModel.__init__
pydantic.error_wrappers.ValidationError: 1 validation error for Resource
id
invalid literal for int() with base 16: 'X8991fd8b65545ff996f8bc1f60f31e0' (type=value_error)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With