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

Construct does not recursively construct BaseModel? #3471

Closed
3 tasks done
thomasaarholt opened this issue Dec 1, 2021 · 9 comments
Closed
3 tasks done

Construct does not recursively construct BaseModel? #3471

thomasaarholt opened this issue Dec 1, 2021 · 9 comments
Assignees
Labels
bug V1 Bug related to Pydantic V1.X

Comments

@thomasaarholt
Copy link

thomasaarholt commented Dec 1, 2021

Checks

  • I added a descriptive title to this issue
  • I have searched (google, github) for similar issues and couldn't find anything
  • I have read and followed the docs and still think this is a bug

Bug

I have built a "nested" Basemodel (e.g, the fields of a basemodel are basemodels themselves), where I have validation that warns in certain cases, and validation that sets some attributes on other cases. I would like to be able to pass this validated model to a very similar BaseModel, without warnings and setting occurring again.

I looked at BaseModel.construct, which is advertised as exactly what I want, but it looks like none of its fields are set to be baseclasses themselves. Is this intentional behaviour? This does not occur if I use BaseModel.parse_obj.

If this is intentional, is there any way I instantiate a nested model without validation?

To be extra clear with the example below: Given a dict data, I would like to create object a without validation occuring.

Output of python -c "import pydantic.utils; print(pydantic.utils.version_info())":

...             pydantic version: 1.8.2
            pydantic compiled: True
                 install path: /usr/local/lib/python3.6/site-packages/pydantic
               python version: 3.6.15 (default, Oct 12 2021, 03:54:14)  [GCC 8.3.0]
                     platform: Linux-5.4.120+-x86_64-with-debian-10.11
     optional deps. installed: ['dotenv', 'typing-extensions']
from pydantic import BaseModel

class Bar(BaseModel):
    y: int
    z: int

class Foo(BaseModel):
    x: Bar

data = {'x':{'y': 1, 'z': 2}}

a = Foo(**data)
print(isinstance(a, BaseModel)) # True
print(isinstance(a.x, BaseModel)) # True

b = Foo.construct(**data) #               <----- Difference is here!
print(isinstance(b, BaseModel)) # True
print(isinstance(b.x, BaseModel)) # False

a.x.y # Works
b.x.y # Error: 'dict' object has no attribute 'y'
@thomasaarholt thomasaarholt added the bug V1 Bug related to Pydantic V1.X label Dec 1, 2021
@thomasaarholt
Copy link
Author

I edited the post for clarify, I realised I written does when I meant does not.

@elda27
Copy link

elda27 commented Dec 3, 2021

You may read the document.
https://pydantic-docs.helpmanual.io/usage/models/

construct()
a class method for creating models without running validation; cf. Creating models without validation

construct method is not run the validation so that this behavior is correct, I think.

@thomasaarholt
Copy link
Author

I'd argue that this is about type casting, not validation. Perhaps I can submit a PR to at least document that?

@elda27
Copy link

elda27 commented Dec 3, 2021

I'm not maintainer so maybe something wrong.
But I seem this section described your suggestion.
https://pydantic-docs.helpmanual.io/usage/validators/#field-checks

@g0di
Copy link

g0di commented Dec 12, 2021

I already discovered that. Construct method do not recursively transform raw data to instances of nested classes inheriting BaseModel. Basically either you instantiate your object with parse_obj or the constructor and it must run validators or you consider that your input data is already valid AND parsed (i.e: it contains instances of your nested models if any)

By the way I would really like a « recursive » construct method as well

@havardthom
Copy link

This is not a bug but intended behaviour, see duplicate here: #1168

I agree that documentation of construct does not reflect its behaviour or use cases very well. And I also would like to see a « recursive » construct.

@dmontagu dmontagu self-assigned this Apr 28, 2023
@dmontagu
Copy link
Contributor

I'll make a note on our v2 documentation plan that we should document this better.

In general it's hard to draw a line on what should be done automatically or not in a construct function; we've chosen the approach that offers the best possible performance when you know you have attributes that are ready for use.

If you want an "intermediate" version that will construct field values without doing any validation besides packaging dicts into models, I would recommend implementing that as a method on a shared base class that calls things recursively.

@dmontagu
Copy link
Contributor

In #6121 I have updated the docs for model_construct. Right now, the implementation is in python (as opposed to validation which is in rust). As a result, the performance gap between model_construct and model_validate/__init__ has been considerably reduced, and for simpler models __init__ may be even faster.

@samuelcolvin has mentioned potentially moving the model_construct logic to pydantic-core which should make it always-faster again (though likely by a significantly smaller margin than was the case in v1). If you want a way to recursively construct models from dicts, I would suggest opening a new issue as a feature request for this, as moving the logic to Rust is the perfect time to add support for this (and will likely make it possible to do this sort of thing without the significant performance overhead that would be present trying to do it in python).

@lig
Copy link
Contributor

lig commented Jun 14, 2023

@dmontagu on the other hand fixing this in Python now is close to trivial

@@ -472,10 +472,12 @@ class BaseModel(metaclass=_model_construction.ModelMetaclass):
         fields_values: dict[str, Any] = {}
         defaults: dict[str, Any] = {}  # keeping this separate from `fields_values` helps us compute `_fields_set`
         for name, field in cls.model_fields.items():
-            if field.alias and field.alias in values:
-                fields_values[name] = values.pop(field.alias)
-            elif name in values:
-                fields_values[name] = values.pop(name)
+            value_name = field.alias if field.alias else name
+            if value_name in values:
+                value = values.pop(value_name)
+                if field.annotation and issubclass(field.annotation, BaseModel):
+                    value = field.annotation.model_construct(**value)
+                fields_values[name] = value
             elif not field.is_required():
                 defaults[name] = field.get_default(call_default_factory=True)
         if _fields_set is None:

I guess, some edge cases could exist and they are always makes it harder 🤷🏻

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug V1 Bug related to Pydantic V1.X
Projects
None yet
Development

No branches or pull requests

6 participants