Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Different behavior of model_validate/model_validate_json with type fields #6573

Closed
1 task done
nikitakuklev opened this issue Jul 11, 2023 · 5 comments · Fixed by #6756
Closed
1 task done

Different behavior of model_validate/model_validate_json with type fields #6573

nikitakuklev opened this issue Jul 11, 2023 · 5 comments · Fixed by #6756
Assignees
Labels
bug V2 Bug related to Pydantic V2 unconfirmed Bug not yet confirmed as valid/applicable

Comments

@nikitakuklev
Copy link

nikitakuklev commented Jul 11, 2023

Initial Checks

  • I confirm that I'm using Pydantic V2 installed directly from the main branch, or equivalent

Description

We are using generic loader classes to serialize/deserialize callable methods and objects. While migrating to v2, noticed discrepancies between parse_raw and model_validate_json that prevented proper validation of type fields. Interestingly, overriding the inputs in @model_validation(mode='before') completely and supplying same dictionary to model_validate and model_validate_json resulted in only the latter raising an exception. So something in the parsing logic is different for (mode='before'), but the error message is seems to indicate it should work. Both methods work with (mode='after').

It is a bit hard to explain coherently, so please refer to example below.

Output:

Use dict loader
model validator before:  {}
obj_type=<class '__main__.Dummy'>
OK
Use v1 json loader
model validator before:  {}
obj_type=<class '__main__.Dummy'>
OK
Use v2 json loader
model validator before:  {}
obj_type=<class '__main__.Dummy'>
Traceback (most recent call last):
  File "<>\bugsv2.py", line 101, in <module>
    test_serialize_loader()
  File "<>\bugsv2.py", line 98, in test_serialize_loader
    misc_class_loader_type.model_validate_json('{}')
  File "<>\pydantic\main.py", line 507, in model_validate_json
    return cls.__pydantic_validator__.validate_json(json_data, strict=strict, context=context)
pydantic_core._pydantic_core.ValidationError: 1 validation error for Loader[Dummy]
object_type
  Input should be a type [type=is_type, input_value=<class '__main__.Dummy'>, input_type=type]

Example Code

import typing 
import pydantic
from pydantic import BaseModel, ConfigDict, model_validator, field_serializer
from typing import Generic, Optional, TypeVar

ObjType = TypeVar("ObjType")

class Dummy:
    pass

class Loader(BaseModel, Generic[ObjType]):
    model_config = ConfigDict(arbitrary_types_allowed=True)
    object: Optional[ObjType] = None
    object_type: Optional[type] = None

    # mode='after' works in all cases
    @model_validator(mode='before')
    def validate_all(cls, values):
        print('model validator before: ', values)
        annotation = cls.model_fields["object"].annotation
        inner_types = typing.get_args(annotation)
        obj_type = inner_types[0] # temp hack, used to be implemented with __fields__['object'].type_
        print(f'{obj_type=}')
        return {"object_type": obj_type}

def test_serialize_loader():
    print(pydantic.version.version_info())
    misc_class_loader_type = Loader[Dummy]

    print('Use dict loader')
    # works
    misc_class_loader_type.model_validate({})
    print('OK')

    print('Use v1 json loader')
    # works
    misc_class_loader_type.parse_raw('{}')
    print('OK')

    print('Use v2 json loader')
    # fails
    misc_class_loader_type.model_validate_json('{}')
    print('OK')
    
test_serialize_loader()

Python, Pydantic & OS Version

pydantic version: 2.0.2
        pydantic-core version: 2.1.2 release build profile
                 install path: <redacted>
               python version: 3.9.16 | packaged by conda-forge | (main, Feb  1 2023, 21:28:38) [MSC v.1929 64 bit (AMD64)]
                     platform: Windows-10-10.0.19045-SP0
     optional deps. installed: ['typing-extensions']

Selected Assignee: @davidhewitt

@nikitakuklev nikitakuklev added bug V2 Bug related to Pydantic V2 unconfirmed Bug not yet confirmed as valid/applicable labels Jul 11, 2023
@davidhewitt
Copy link
Contributor

This is an interesting case.

The interesting bit of the pydantic_core schema for type comes out as:

{
  'type': 'custom-error',
  'schema': {'type': 'is-instance', 'cls': type},
  'custom_error_type': 'is_type',
  'custom_error_message': 'Input should be a type'
}

To me, what it looks like is happening is that is-instance is refusing to check, because this is a JSON input and it's assumed that in JSON inputs the types are meaningless. This error then gets swallowed by the custom-error wrapper, and gives an overall unhelpful output.

At the very least we should improve the error message, potentially we can even just support this case outright.

@davidhewitt
Copy link
Contributor

davidhewitt commented Jul 12, 2023

Testing locally, if I change the check in is-instance to use input.is_python() instead of extra.mode == Python, this succeeds:

https://github.com/pydantic/pydantic-core/blob/main/src/validators/is_instance.rs#L67

This is because the before validator has the effect of coercing the original JSON input to Python.

It's not clear to me whether changing that is a bad idea? Should instead adjust the schema for type? cc @adriangb

@adriangb
Copy link
Member

It's not clear to me whether changing that is a bad idea?

I'm not sure either. I think changing the schema for the arbitrary types fallback to ignore json makes more sense.

@adriangb
Copy link
Member

This is what I think (or would like to) be happening behind the scenes:

from typing import Any

from pydantic_core import SchemaValidator, core_schema


class Dummy:
    pass

def set_type(v: dict[str, Any]) -> dict[str, Any]:
    return {'object_type': Dummy}

cs = core_schema.no_info_before_validator_function(
    set_type,
    core_schema.typed_dict_schema(
        {
            'object_type': core_schema.typed_dict_field(
                core_schema.json_or_python_schema(
                    json_schema=core_schema.any_schema(),
                    python_schema=core_schema.is_instance_schema(type),
                )
            )
        }
    )
)

v = SchemaValidator(cs)

assert v.validate_json('{}') == {'object_type': Dummy}

@adriangb
Copy link
Member

MRE:

from typing import Any
from pydantic import BaseModel, ConfigDict, model_validator


class Dummy:
    pass


class Loader(BaseModel):
    model_config = ConfigDict(arbitrary_types_allowed=True)
    object_type: type

    @model_validator(mode='before')
    def validate_all(cls, _values: dict[str, Any]) -> dict[str, Any]:
        return {"object_type": Dummy}


assert Loader.model_validate_json('{}') == {'object_type': Dummy}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug V2 Bug related to Pydantic V2 unconfirmed Bug not yet confirmed as valid/applicable
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants