qangdev
qangdev

Reputation: 394

Pydantic Model: Convert UUID to string when calling .dict()

Thank you for your time.

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? Thank you.


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': ''}

Upvotes: 13

Views: 19476

Answers (6)

Yaakov Bressler
Yaakov Bressler

Reputation: 12158

In V2, I solve this by extending the properties of UUID with an annotated type:

from typing import Optional, Annotated
from uuid import UUID, uuid4
from pydantic import BaseModel, Field, BeforeValidator, PlainSerializer


BetterUUID = Annotated[
    UUID,
    BeforeValidator(lambda x: UUID(x) if isinstance(x, str) else x),
    PlainSerializer(lambda x: str(x)),
    Field(
        description="Better annotation for UUID, parses from string format. Serializes to string format."
    ),
]

class TestModel(BaseModel):
    id: BetterUUID = Field(default_factory=uuid4)


# ----------------------------

# Instantiate with defaults
record = TestModel()
print(record.model_dump())

# Accepts string or UUID
some_id = "d4ea4a2f-9731-4dbe-a078-945d9a9d7fb2"
assert TestModel(id=some_id) == TestModel(id=UUID(some_id))

# Output is always a string too
assert some_id == \
       TestModel(id=some_id).model_dump()["id"] == \
       TestModel(id=UUID(some_id)).model_dump()["id"]

Upvotes: 1

focus zheng
focus zheng

Reputation: 380

there is a simple way to help to convert your data

from datetime import datetime
from uuid import UUID, uuid4
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel, Field

fake_db = {}


class Item(BaseModel)
    uid: UUID = Field(default_factory=uuid4)
    updated: datetime = Field(default_factory=datetime_now)

m_data = Item()
jsonable_encoder(m_data)
{
 'updated': '2024-05-14T03:06:25.427068+00:00',
 'uid': '62301346-cf2f-48f4-a8be-4c019a6f4f7c'
}

jsonable_encoder can work for you.

Upvotes: 0

David Tabla
David Tabla

Reputation: 153

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'}

Upvotes: 5

Thomas Ruff
Thomas Ruff

Reputation: 30

I found a easy way, to convert UUID to string using .dict():

from uuid import UUID
from pydantic import BaseModel


class Person(BaseModel):
    id: UUID
    name: str
    married: bool


person = Person(id='a746f0ec-3d4c-4e23-b6f6-f159a00ed792', name='John', married=True)

print(json.loads(person.json()))

Result:

{'id': 'a746f0ec-3d4c-4e23-b6f6-f159a00ed792', 'name': 'John', 'married': True}

Upvotes: -1

RaamEE
RaamEE

Reputation: 3517

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)

Upvotes: 4

John Sturgeon
John Sturgeon

Reputation: 320

You don’t need to convert a UUID to a string for mongodb. You can just add the record to the DB as a UUID and it will save it as Binary.

Here is an example creating a quick UUID and saving it directly to the DB:

    from pydantic import BaseModel
    from uuid import UUID, uuid4


    class Example(BaseModel):
        id: UUID
        note: str


    def add_uuid_to_db():
        #database = <get your mongo db from the client>
        collection = database.example_db
        new_id: UUID = uuid4()
        new_record = {
            'id': new_id,
            'note': "Hello World"
        }
        new_object = Example(**new_record)
        collection.update_one(
            filter={},
            update={"$set": new_object.dict()},
            upsert=True
        )


    if __name__ == '__main__':
        add_uuid_to_db()

And here is the resulting record:

    {
      "_id": {
        "$oid": "611d1d0d6e00f4849c14a792"
      },
      "id": {
        "$binary": "jyxxsFKaToupb55VUKm0kw==",
        "$type": "3"
      },
      "note": "Hello World"
    }

Upvotes: -4

Related Questions