marshmallow-sqlalchemy
marshmallow-sqlalchemy copied to clipboard
Loading or validating nested objects fails when their ids are dump_only or hidden
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?
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?