team23 / pydantic-partial

Create partial models from pydantic models
MIT License
46 stars 7 forks source link

Feature Request: Support custom field validators #23

Open alexdrydew opened 11 months ago

alexdrydew commented 11 months ago

Hi! First of all, thank you for your work, the library provides a really handy way to reduce boilerplate for partial models.

Unfortunately, I faced a blocker for using this library in my case: currently pydantic partial discards metadata from FieldInfo which contains custom validation logic. It would be great if the pydantic-partial library introduced a way to call custom field validators if the value is present.

Here is an example of a possible usage case:

from datetime import timedelta
from typing import Annotated, Any

from pydantic import BaseModel, PlainSerializer, WrapValidator
from pydantic.functional_validators import ModelWrapValidatorHandler
from pydantic_partial import create_partial_model

def timedelta_validator(value: Any, handler: ModelWrapValidatorHandler[timedelta]) -> timedelta:
    if isinstance(value, (timedelta, str)):
        return handler(value)
    elif isinstance(value, (float, int)):
        return timedelta(seconds=value)
    else:
        raise ValueError

TimedeltaSeconds = Annotated[timedelta, PlainSerializer(lambda td: td.total_seconds()), WrapValidator(timedelta_validator)]

class MyModel(BaseModel):
    t: TimedeltaSeconds

MyModelPartial = create_partial_model(MyModel)
assert MyModel(t=timedelta(seconds=10)).model_dump()['t'] == 10.  # ok
assert MyModelPartial(t=timedelta(seconds=10)).model_dump()['t'] == 10.  # error

Thank you!

Version info

pydantic-partial==0.5.2

pydantic version: 2.4.2 pydantic-core version: 2.10.1 pydantic-core build: profile=release pgo=false python version: 3.11.4 (main, Aug 28 2023, 23:10:28) [Clang 14.0.3 (clang-1403.0.22.14.1)] platform: macOS-14.0-arm64-arm-64bit related packages: mypy-1.6.0 typing_extensions-4.8.0 pydantic-settings-2.0.3 fastapi-0.101.1

ddanier commented 10 months ago

@alexdrydew Sorry, I had no time to look into this - but I will do that. I don't think it's that simple...

Currently we want to skip validators, because those might just break when the type changes due to what pydantic-partial does. Your code for example should also accept None in the partial model version (like MyModelPartial(t=None)), which I think (haven't tested this yet) the validator would prevent by triggering a ValueError. So this might (!) introduce strange side effects. 😉

ADR-007 commented 7 months ago

Hi @ddanier, thank you for your answer.

Skipping validators makes the partial models a bit useless in my case :( So, I added PR with this feature. Could you please take a look? PR

AlmogBaku commented 4 months ago

@alexdrydew Sorry, I had no time to look into this - but I will do that. I don't think it's that simple...

Currently we want to skip validators, because those might just break when the type changes due to what pydantic-partial does. Your code for example should also accept None in the partial model version (like MyModelPartial(t=None)), which I think (haven't tested this yet) the validator would prevent by triggering a ValueError. So this might (!) introduce strange side effects. 😉

I guess that we should trigger field validation only if not None (and was not Optional originally)

ddanier commented 4 months ago

I guess that we should trigger field validation only if not None (and was not Optional originally)

@AlmogBaku I hope you understand that this sounds like a very bad developer experience. Sometimes the validators are executed, sometimes not. You might have a validator das explicitly wants to handle None values for example. This would then break.

I didn't come up with a good solution so far, sorry!

ADR-007 commented 4 months ago

@AlmogBaku if you want to have the validation always be enabled, you may want to try my version of this library that I did for that case: pydantic-strict-partial.