marshmallow-sqlalchemy icon indicating copy to clipboard operation
marshmallow-sqlalchemy copied to clipboard

Loading or validating nested objects fails when their ids are dump_only or hidden

Open kshade opened this issue 7 years ago • 1 comments

My declarative database model looks like this:

class X(db.Model):
    id = db.Column(db.String(128), primary_key=True)
    yref = db.relationship('Y', backref='x', uselist=False, lazy='joined')

    def __init__(self, myid):
        self.id = myid


class Y(db.Model):
    id = db.Column(db.String(128), db.ForeignKey('x.id'), primary_key=True)
    value = db.Column('value', db.SmallInteger, nullable=False, default=0)

    def __init__(self, x_id, value=0):
        self.id = x_id
        self.value = value

I have two schemes like this:

class YSchema(ma.ModelSchema):
    value = fields.Integer()

    class Meta:
        model = models.YFields
        fields = ('value')


class XSchema(ma.ModelSchema):
    id = fields.String(dump_only=True)
    y = fields.Nested(YSchema, attribute='yref', many=False)

    class Meta:
        model = models.X
        fields = ('id', 'y')

When I use jsonify, I get output like this:

{
  "id": "X874", 
  "y": {
    "value": 0,
  },
}

Which is exactly what I want, but when I then try to modify y.value using the same json or try to validate the input like this:

result = models.x.query.filter_by(id=xid).first()
xschema.load(request.get_json(), instance=result)
xschema.validate(request.get_json())

It always results in this error:

Traceback (most recent call last):
  File "/projectpath/lib/python3.4/site-packages/flask/app.py", line 1997, in __call__
    return self.wsgi_app(environ, start_response)
  File "/projectpath/lib/python3.4/site-packages/flask/app.py", line 1985, in wsgi_app
    response = self.handle_exception(e)
  File "/projectpath/lib/python3.4/site-packages/flask_restful/__init__.py", line 273, in error_router
    return original_handler(e)
  File "/projectpath/lib/python3.4/site-packages/flask/app.py", line 1540, in handle_exception
    reraise(exc_type, exc_value, tb)
  File "/projectpath/lib/python3.4/site-packages/flask/_compat.py", line 32, in reraise
    raise value.with_traceback(tb)
  File "/projectpath/lib/python3.4/site-packages/flask/app.py", line 1982, in wsgi_app
    response = self.full_dispatch_request()
  File "/projectpath/lib/python3.4/site-packages/flask/app.py", line 1614, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/projectpath/lib/python3.4/site-packages/flask_restful/__init__.py", line 273, in error_router
    return original_handler(e)
  File "/projectpath/lib/python3.4/site-packages/flask/app.py", line 1517, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/projectpath/lib/python3.4/site-packages/flask/_compat.py", line 32, in reraise
    raise value.with_traceback(tb)
  File "/projectpath/lib/python3.4/site-packages/flask/app.py", line 1612, in full_dispatch_request
    rv = self.dispatch_request()
  File "/projectpath/lib/python3.4/site-packages/flask/app.py", line 1598, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/projectpath/lib/python3.4/site-packages/flask_restful/__init__.py", line 480, in wrapper
    resp = resource(*args, **kwargs)
  File "/projectpath/lib/python3.4/site-packages/flask/views.py", line 84, in view
    return self.dispatch_request(*args, **kwargs)
  File "/projectpath/lib/python3.4/site-packages/flask_restful/__init__.py", line 595, in dispatch_request
    resp = meth(*args, **kwargs)
  File "/projectpath/project/project/views_api.py", line 36, in put
    print(device_schema.validate(request.get_json()))
  File "/projectpath/lib/python3.4/site-packages/marshmallow_sqlalchemy/schema.py", line 194, in validate
    return super(ModelSchema, self).validate(data, *args, **kwargs)
  File "/projectpath/lib/python3.4/site-packages/marshmallow/schema.py", line 620, in validate
    _, errors = self._do_load(data, many, partial=partial, postprocess=False)
  File "/projectpath/lib/python3.4/site-packages/marshmallow/schema.py", line 660, in _do_load
    index_errors=self.opts.index_errors,
  File "/projectpath/lib/python3.4/site-packages/marshmallow/marshalling.py", line 295, in deserialize
    index=(index if index_errors else None)
  File "/projectpath/lib/python3.4/site-packages/marshmallow/marshalling.py", line 68, in call_and_store
    value = getter_func(data)
  File "/projectpath/lib/python3.4/site-packages/marshmallow/marshalling.py", line 288, in <lambda>
    data
  File "/projectpath/lib/python3.4/site-packages/marshmallow/fields.py", line 265, in deserialize
    output = self._deserialize(value, attr, data)
  File "/projectpath/lib/python3.4/site-packages/marshmallow/fields.py", line 465, in _deserialize
    data, errors = self.schema.load(value)
  File "/projectpath/lib/python3.4/site-packages/marshmallow_sqlalchemy/schema.py", line 186, in load
    ret = super(ModelSchema, self).load(data, *args, **kwargs)
  File "/projectpath/lib/python3.4/site-packages/marshmallow/schema.py", line 580, in load
    result, errors = self._do_load(data, many, partial=partial, postprocess=True)
  File "/projectpath/lib/python3.4/site-packages/marshmallow/schema.py", line 685, in _do_load
    original_data=data)
  File "/projectpath/lib/python3.4/site-packages/marshmallow/schema.py", line 855, in _invoke_load_processors
    data=data, many=many, original_data=original_data)
  File "/projectpath/lib/python3.4/site-packages/marshmallow/schema.py", line 957, in _invoke_processors
    data = utils.if_none(processor(data), data)
  File "/projectpath/lib/python3.4/site-packages/marshmallow_sqlalchemy/schema.py", line 174, in make_instance
    return self.opts.model(**data)
TypeError: __init__() missing 1 required positional argument: 'x_id'

The same also happens when I do send and expose y.id but make it dump_only (I don't want people to change that, ever), it only works when it can be changed. I've poked around with the debugger and saw that id just gets filtered out in the last steps, never reaching the constructor.

Directly (without Marshmallow) writing to the nested Y works just fine, like this for example:

result.yref.value = 93
db.session.merge(result)
db.session.commit()

Is there any way to do this that I'm missing, or is this simply not possible right now?

kshade avatar Jul 09 '17 05:07 kshade

Even when I just filter out the id field with the "only" argument from fields.Nested this happens, I seem to just have to expose the y.id to the client and make sure that it's in the Y part of the client request (not having it in the X part doesn't matter). Injecting it in the view works (and ensures that the client can't overwrite it), but that can't be the proper solution, can it?

kshade avatar Jul 09 '17 17:07 kshade