BeanieODM / beanie

Asynchronous Python ODM for MongoDB
http://beanie-odm.dev/
Apache License 2.0
1.99k stars 211 forks source link

[BUG] Decimal type on save #725

Closed ID2343242 closed 6 months ago

ID2343242 commented 11 months ago

Describe the bug When using a Decimal type in a class as below, the insert works fine, the save also adds the Document to the database but it throws a ValidationError as shown below.

To Reproduce

class Product(Document): name: str price: Decimal = Field(decimal_places=3) product = Product(name="Apple", price=Decimal('1.99')) await product.save()

Expected behavior The save function should not throw the validation exception and if the Document does not exist should operate in the same way as insert.

Additional context Exception has occurred: ValidationError 1 validation error for Product price Decimal input should be an integer, float, string or Decimal object [type=decimal_type, input_value=Decimal128('1.99'), input_type=Decimal128] For further information visit https://errors.pydantic.dev/2.3/v/decimal_type File "/test.py", line 120, in main await product.save() File "/test.py", line 126, in asyncio.run(main()) pydantic_core._pydantic_core.ValidationError: 1 validation error for Product price Decimal input should be an integer, float, string or Decimal object [type=decimal_type, input_value=Decimal128('1.99'), input_type=Decimal128] For further information visit https://errors.pydantic.dev/2.3/v/decimal_type

ID2343242 commented 11 months ago

I have just tried using find on this same Document and it throws the same validation error. So currently I can only get insert to work with a Document containing a Decimal field.

roman-right commented 11 months ago

Hi! Thank you for the catch. I'll check and fix it this/next week.

gsakkis commented 11 months ago

@ID2343242 try replacing the Decimal field annotation with beanie.DecimalAnnotation.

maxktz commented 9 months ago

@ID2343242 try replacing the Decimal field annotation with beanie.DecimalAnnotation.

thank brocha

roman-right commented 9 months ago

Hi @ID2343242 , Could you please try beanie.DecimalAnnotation ?

ogtega commented 8 months ago

I came up with this solution using a decorator:

from typing import Any, Callable, TypeVar, cast

import bson
from pydantic import BaseModel, create_model, model_validator

T = TypeVar("T", bound="BaseModel")

def beanie_decimal(*fields: str) -> Callable[[type[T]], type[T]]:
    def validator(cls: type[T], v: Any) -> Any:
        for f in (f.split(".") for f in fields):
            current = v

            while (field := f.pop(0)) and current:
                if not len(f):
                    if value := current.get(field, None):
                        current[field] = (
                            value.to_decimal()
                            if isinstance(value, bson.Decimal128)
                            else value
                        )
                    break

                current = current.get(field, None)

        return v

    def decorator(model: type[T]) -> type[T]:
        return create_model(
            model.__name__,
            __base__=model,
            __module__=model.__module__,
            __validators__={
                "beanie_decimal": cast(
                    classmethod, model_validator(mode="before")(validator)
                )
            },
        )

    return decorator

@beanie_decimal("price")
class Product(Document):
  name: str
  price: Decimal = Field(decimal_places=3)

product = Product(name="Apple", price=Decimal('1.99'))
await product.save()

The decorator supports multiple fields being passed in and allows you to define nested model paths.

github-actions[bot] commented 7 months ago

This issue is stale because it has been open 30 days with no activity.

github-actions[bot] commented 6 months ago

This issue was closed because it has been stalled for 14 days with no activity.