Fastapi: [QUESTION] How can I prevent passing float and bool to integer fields

Created on 21 Aug 2019  Â·  10Comments  Â·  Source: tiangolo/fastapi

Integer fields convert bool and float values to integer (1.5, true -> 1, 0) without error raising.

question

All 10 comments

It is a pydantic question imho
Take a look at https://github.com/samuelcolvin/pydantic/issues/360
Or 284

Le mer. 21 août 2019 à 4:00 PM, Vadim Suharnikov notifications@github.com
a écrit :

Integer fields convert bool and float values to integer (1.5, true -> 1,
0) without error raising.

—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/tiangolo/fastapi/issues/453?email_source=notifications&email_token=AAINSPUDZNDHHTPD2E5USJTQFVDA3A5CNFSM4IOHLQE2YY3PNVWWK3TUL52HS4DFUVEXG43VMWVGG33NNVSW45C7NFSM4HGQW2IA,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAINSPSZIRQ7SWXCBO77UDDQFVDA3ANCNFSM4IOHLQEQ
.

@deterok Look at the pydantic docs on validator. You'd want to add a validator (with pre=True to prevent the cast) that performs the check you want.

It's not clear whether you want to check if the input is of type int, or if it is allowed to be a float as long as when rounded to an int it is equal to itself. Either way, it can be done inside a validator, just raise a ValueError or TypeError if you want to fail validation, and return the original value (or the int-casted version of it) if you want it to pass.

@deterok,

Pydantic already has StrictBool and StrictStr that require passed value to be of str or bool type respectively. You can complement it with StrictInt as follows:

class StrictInt(int):
    @classmethod
    def __get_validators__(cls) -> Generator[Callable[[Any], int], None, None]:
        # yield int_validator
        yield cls.validate

    @classmethod
    def validate(cls, v: Any) -> int:
        if not isinstance(v, int):
            raise IntegerError()
        return v

@samuelcolvin - wouldn't it be nice to have StrictInt as core part of Pydantic (similar to existing StrictBool / StrictStr)? Will you accept PR for that?

@haizaar to that point, I think it probably makes sense to just have a generic way to achieve StrictX. Something like:

T = TypeVar("T")
def strict_type(type_: Type[T]) -> Type[T]:
    class StrictType(type_):
        def __get_validators__(cls) -> Generator[Callable[[Any], int], None, None]:
            # yield int_validator
            yield cls.validate

        @classmethod
        def validate(cls, v: Any) -> int:
            if not isinstance(v, type_):
                raise TypeError(f"{v} was not a {type_}")
            return v
    return StrictType

StrictInt = strict_type(int)

This probably needs a little more massaging to get it to play nice with mypy, etc; maybe it should be a generic type (though this might be more awkward for consumers):

T = TypeVar("T")
class StrictType(Generic[T]):
    type_: ClassVar[Type[T]]
    def __get_validators__(cls) -> Generator[Callable[[Any], int], None, None]:
        # yield int_validator
        yield cls.validate

    @classmethod
    def validate(cls, v: Any) -> int:
        if not isinstance(v, cls.type_):
            raise TypeError(f"Received {type(v)}; required {cls.type_}")
        return v

class StrictInt(StrictType[int]):
    type_ = int

But a generic approach might be better than a proliferation of StrictX classes.

@dmontagu I think your idea is brilliant.

I prefer creator function approach - anyone can create classes for strict instance checking to their liking. I took your example one step forward. Still mypy complains :( Any mypy gurus here to help?

Here is the complete code:

from typing import Any, Callable, Generator, Type, TypeVar

from pydantic import BaseModel


T = TypeVar("T")


def strict_type(type_: Type[T]) -> Type[T]:
    class StrictType(type_):
        @classmethod
        def __get_validators__(cls) -> Generator[Callable[[Any], T], None, None]:
            yield cls.validate

        @classmethod
        def validate(cls, v: Any) -> T:
            if not isinstance(v, type_):
                raise TypeError(f"Expected instance of {type_}, got {type(v)} instead")
            return v
    return StrictType


StrictInt = strict_type(int)


class MyModel(BaseModel):
    foo: StrictInt


print(MyModel(foo=10).json(indent=2))

Any mypy complains about:

/tmp/foo.py:10: error: Variable "type_" is not valid as a type
/tmp/foo.py:10: error: Invalid base class "type_"
/tmp/foo.py:27: error: Variable "union2.StrictInt" is not valid as a type

Yeah, I don’t think mypy lets you do this — it really doesn’t like dynamically generated types (for good reason, I suppose). They can be handled via mypy plugin, but that’s also kind of awkward.

That’s why I proposed the generic class approach, which does play nice with mypy, but definitely feels more awkward to use (since you have to declare a class each time). Maybe there’s a version of this that uses a custom root...

At any rate, this discussion probably should be moved to a pydantic issue.

Let's create an issue on pydantic to discuss. I agree a general implementation would be the best solution, we might then create StrictInt and StrictFloat etc. for easy use.

However note the following:

Pydantic is not a validation library, it's parsing library. It makes guarantees about the form of data you get out, rather than making a guarantee about the form of the data you put in. That's how pydantic started, it's decision I'm happy with and it is not going to change. If you use pydantic hoping it will do strict validation you will be disappointed.

Please see https://github.com/samuelcolvin/pydantic/issues/578.

Created https://github.com/samuelcolvin/pydantic/issues/780

@samuelcolvin The difference between validation vs parsing library never occurred to me until now. All I tried is to define a union of str, bool, and int that manages to keep the original data type coming from JSON.
I looks to me that one should be able to define a model in such way that de/ser cycle, i.e. JSON->pydantic model -> JSON is idempotent. This looks like a parsing issue to me. Do you see different?

(I don't want to start validation vs parsing argument, just would like to understand your opinion and approach on this specific matter.)

@deterok yeah, that's how standard Python works. And Pydantic is quite Pythonic, so it uses the same behavior, it tries first to make it work before complaining. You can still use Pydantic's strict versions if you need them.

But for example, have in mind that if you declare something like a path parameter /users/{users_id}/ and declare user_id: int, the URL spec (and HTTP) only define URLs as strings. So that user_id will come as a string, not as an int. There's no way to "validate" it is exactly an int without trying to convert it to an int.

That automatic conversion is what allows declaring more specific data types for Path, Query, Header, and Cookie parameters, that otherwise would be just strings.

I've done StrictInt. Thank you for the help.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

tsdmrfth picture tsdmrfth  Â·  3Comments

KoduIsGreat picture KoduIsGreat  Â·  3Comments

updatatoday picture updatatoday  Â·  3Comments

laith43d picture laith43d  Â·  3Comments

DrPyser picture DrPyser  Â·  3Comments