miLibris / flask-rest-jsonapi

Flask extension to build REST APIs around JSONAPI 1.0 specification.
http://flask-rest-jsonapi.readthedocs.io
MIT License
597 stars 153 forks source link

Support nested attributes #70

Closed jcampbell closed 5 years ago

jcampbell commented 7 years ago

The JSONAPI spec specifically leaves latitude for the types of an object's attributes, including attributes of an object that are themselves an array or have some other join conditions but are not related objects (to ensure they cannot be individually addressed and/or are required).

That allows us to express one-to-many semantics even for required attributes, and to guarantee those attributes are present when an object is created or updated.

For example, we might want to require "tags" for people objects:

{
  "data": {
    "attributes": {
       "name": "James",
       "person_tags": [
                {
                    "key": "k1",
                    "value": "v1"
                },
                {
                    "key": "k2",
                    "value": "v2"
                }
            ]
    },
    "type": "person"
}

SQLAlchemy and Marshmallow support this feature via the relationship option and List and Nested field types, but the current create_object cannot handle this use case.

class Person_Tags(db.Model):
    id = db.Column(db.Integer, db.ForeignKey('person.id'), primary_key=True, index=True)
    key = db.Column(db.String, primary_key=True)
    value = db.Column(db.String, primary_key=True)

class Person(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String)
    person_tags= db.relationship("Person_Tags")

## Note we inherit from Marshmallow schema instead of Marshmallow JSONAPI schema here
class PersonTagsSchema(mSchema):
    class Meta:
        type_ = 'person_tags'

    id = fields.Str(dump_only=True)
    key = fields.Str()
    value = fields.Str()

class PersonSchema(Schema):
    class Meta:
        type_ = 'person'
        self_view = 'person_detail'
        self_view_kwargs = {'id': '<id>'}
        self_view_many = 'person_list'

    id = fields.Str(dump_only=True)
    name = fields.Str()
    person_tags = fields.List(fields.Nested(PersonTagsSchema))
hellupline commented 6 years ago

you can create a attribute on the schema with "any" type of data using the marshmallow Function field: ( I tried using marshmallow.fields.Nested, but marshmallow_jsonapi requires a ID field ).

    test = ma.fields.Function(lambda obj: [{'msg': 'hello'}])

if you Nested schema has id

    test = ma.fields.Nested('TestSchema', many=True)

class TestSchema(ma_flask.Schema):
    class Meta:
        type_ = 'test'

    id = ma.fields.Integer(as_string=True, dump_only=True)
    msg = ma.fields.String()
jcampbell commented 6 years ago

I'm certainly may have missed a simpler solution and would love to use it...do you have a working example using the Nested(OtherSchema, many=True) approach?

For me, that works fine for reading existing values, but does not allow posting/patching new values. It looked to me like that was because of the way the new object is created in alchemy.py--if the schema declares the field as a Relationship than it is treated specially and the related schema object is created, but otherwise it tries to build the object directly by just unpacking the arguments.