Skip to content

[13.0][FIX] base_rest_datamodel: modify validation of responses to allow dump_only fields#169

Merged
OCA-git-bot merged 3 commits into
OCA:13.0from
fdegrave:13.0
Oct 8, 2021
Merged

[13.0][FIX] base_rest_datamodel: modify validation of responses to allow dump_only fields#169
OCA-git-bot merged 3 commits into
OCA:13.0from
fdegrave:13.0

Conversation

@fdegrave

Copy link
Copy Markdown

In marshmallow, dump (serialization) and load (deserialization) operations using the same schema are not symmetrical in general. That is because fields can be declared as dump_only or load_only. That means that if you try to load data you just dumped it could fail, and vice-versa.

And that is exactly what is (was) wrong with this validation: the validate method on the ModelClass uses load to validate the data. That means that if you declare some fields to be dump_only, this validation necessarily crashes.

Moreover, it seems that this validation is not only incorrect but also useless (if it worked as intended), since it bases its verification on the very same schema as the one that was just used to serialize the data.

@fdegrave fdegrave changed the title [FIX] base_rest_datamodel: remove incorrect validation of responses [13.0][FIX] base_rest_datamodel: remove incorrect validation of responses Jun 23, 2021
@lmignon

lmignon commented Jun 30, 2021

Copy link
Copy Markdown
Contributor

@fdegrave Validation is IMO a requirement since I want to be sure that the data returned by the service are conform to the schema. I understand that the current one is not right but how can we provide such a validation.?

@fdegrave

fdegrave commented Jun 30, 2021

Copy link
Copy Markdown
Author

The simple fact that the schema is used to dump the data is already a validation. Here is a simple example:

from marshmallow import Schema, fields


class MySchema(Schema):
    my_string = fields.Str()
    my_integer = fields.Integer()
    

class DataClass:
    def __init__(self, my_string, my_integer):
        self.my_string = my_string
        self.my_integer = my_integer

Now let's dump a valid instance:

>>> instance_ok = DataClass(my_string="Yes", my_integer=5)
>>> MySchema().dump(instance_ok)
{'my_string': 'Yes', 'my_integer': 5}

As expected. Now let's do it with an instance that does not conform to the schema:

>>> instance_bad = DataClass(my_string=1, my_integer="No")
>>> MySchema().dump(instance_bad)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/fd/.virtualenvs/odoo/lib/python3.8/site-packages/marshmallow/schema.py", line 547, in dump
    result = self._serialize(processed_obj, many=many)
  File "/home/fd/.virtualenvs/odoo/lib/python3.8/site-packages/marshmallow/schema.py", line 515, in _serialize
    value = field_obj.serialize(attr_name, obj, accessor=self.get_attribute)
  File "/home/fd/.virtualenvs/odoo/lib/python3.8/site-packages/marshmallow/fields.py", line 318, in serialize
    return self._serialize(value, attr, obj, **kwargs)
  File "/home/fd/.virtualenvs/odoo/lib/python3.8/site-packages/marshmallow/fields.py", line 898, in _serialize
    ret = self._format_num(value)  # type: _T
  File "/home/fd/.virtualenvs/odoo/lib/python3.8/site-packages/marshmallow/fields.py", line 873, in _format_num
    return self.num_type(value)
ValueError: invalid literal for int() with base 10: 'No'

As you can see, validation is included in the dump method, since the schema is used during the serialization. No need to try to re-validate it afterwards.

@fdegrave

fdegrave commented Jun 30, 2021

Copy link
Copy Markdown
Author

However you are right that not everything is validated (see there for example). In the same thread, people have the exact same issue we are discussing here (link) but they don't come up with a solution (unfortunately)...

One thing that we could do is list the names of dump_only fields, and pass them to the ModelClass.validate method under the partial parameter (which can be a list of field names). Not ideal (because dump_only fields are not validated at all) but better than what we currently have (because at least it would not fail).

I'll try that tomorrow.

@fdegrave

fdegrave commented Jun 30, 2021

Copy link
Copy Markdown
Author

What they do there is instantiating the schema with a unknown=EXCLUDE parameter.

Therefore a second solution would be to add a new unknown parameter to validate (define in marshmallow_objects) and override it to modify the way the schema is instantiated. Here is the current method:

    @classmethod
    def validate(cls, data, context=None, many=None, partial=None):
        schema = cls.__get_schema_class__(context=context, partial=partial)
        return schema.validate(data, many=many, partial=partial)

And we could simply do:

    @classmethod
    def validate(cls, data, context=None, many=None, partial=None, unknown=None):
        schema = cls.__get_schema_class__(context=context, partial=partial, unknown=unknown)
        return schema.validate(data, many=many, partial=partial)

And modify the current validation with:

from marshmallow import EXCLUDE

...

errors = ModelClass.validate(json, many=self._is_list, unknown=EXCLUDE)

Since after some thoughts I doubt the first solution would work, I'll try this one tomorrow.

@fdegrave

fdegrave commented Jul 1, 2021

Copy link
Copy Markdown
Author

I implemented my latest proposition, but it was trickier than I though because of nested models (no control on the schema for those, no way to easily override them during validation -- had to be creative, I think it's the cleanest I can do).

@fdegrave

fdegrave commented Jul 1, 2021

Copy link
Copy Markdown
Author

I rebased my branch after the fix of the tests have been merged. Hopefully everything should work OK.

@fdegrave

fdegrave commented Jul 1, 2021

Copy link
Copy Markdown
Author

Nearly there :-D

@fdegrave fdegrave changed the title [13.0][FIX] base_rest_datamodel: remove incorrect validation of responses [13.0][FIX] base_rest_datamodel: modify validation of responses to allow dump_only fields Jul 1, 2021
@fdegrave

fdegrave commented Jul 1, 2021

Copy link
Copy Markdown
Author

Victory!

@lmignon lmignon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fdegrave Thank you for this fix and all your experiments to provide the right one. First of all, feel free to add your name into the list of contributors!
Before merging I propose to add some tests and change the existing one (see my comments)

Comment thread datamodel/core.py
Comment thread base_rest_demo/tests/test_new_api.py Outdated
Comment thread datamodel/core.py
Comment thread base_rest_datamodel/tests/test_response.py Outdated
fdegrave and others added 3 commits August 25, 2021 14:24
In marshmallow, `dump` (serialization) and `load` (deserialization) operations using the same schema are not symmetrical in general. That is because fields can be declared as `dump_only` or `load_only`. That means that trying to `load` data you just `dumped` can result in errors, and vice-versa.

And that is exactly what is (was) wrong with this validation: the `validate` method on the `ModelClass` uses `load` to validate the data. That means that if you declared some fields to be `dump_only`, this validation necessarily crash.

Moreover, it seems that this validation is not only incorrect but also useless (if it worked as intended), since it bases its verification on the very same schema as the one that was just used used to serialize the data.

[FIX] base_rest_datamodel: change response validation to use a schema with `unknown=EXCLUDE`

This modification prevents fields that are declared as `dump_only` to mess with the validation (`dump_only` fields are seen as "unknown" during validation, since marshmallow validation is designed to be used during loading only).

[FIX] base_rest_datamodel: change response validation to use a schema with `unknown=EXCLUDE`

Nested model required extra care because the `unknown=EXCLUDE` is not passed down to the nested schemas when validating data, and this behavior could not easily be overridden. I ended up removing `dump_only` fields from value during `NestedModel` deserialization.

[ADD] base_rest_demo: add test for new api

[ADD] base_rest_demo: test
[FIX] datamodel: allow to instantiate datamodel with dump_only fields as well
[IMP] datamodel: improved 'validate' method, propagating 'unknown' value to nested schemas

[IMP] base_rest_demo: rename test class
…modelRegistryCase" and move them from base_rest_demo to base_rest_datamodel
@fdegrave

Copy link
Copy Markdown
Author

Is there anything still preventing the merge?

@lmignon

lmignon commented Oct 8, 2021

Copy link
Copy Markdown
Contributor

/ocabot merge patch

@OCA-git-bot

Copy link
Copy Markdown
Contributor

On my way to merge this fine PR!
Prepared branch 13.0-ocabot-merge-pr-169-by-lmignon-bump-patch, awaiting test results.

@lmignon

lmignon commented Oct 8, 2021

Copy link
Copy Markdown
Contributor

times to merge a pending merge for a long time

@OCA-git-bot
OCA-git-bot merged commit b63ba88 into OCA:13.0 Oct 8, 2021
@OCA-git-bot

Copy link
Copy Markdown
Contributor

Congratulations, your PR was merged at 5362630. Thanks a lot for contributing to OCA. ❤️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants