apryor6 / flask_accepts

Easy, opinionated Flask input/output handling mixing Marshmallow with flask-restx
BSD 3-Clause "New" or "Revised" License
169 stars 40 forks source link

Marshmallow Two-way Nesting Schemas Validation #55

Open froggylab opened 4 years ago

froggylab commented 4 years ago

In order to avoid circular import in a flask/marshmallow project, it's possible to reference the Nested field by using its name (as described here https://marshmallow.readthedocs.io/en/latest/nesting.html#two-way-nesting)

Unfortunately, flask_accepts doesn't support it :

 File env/lib/python3.8/site-packages/flask_accepts/decorators/decorators.py", line 110, in decorator
body = for_swagger(
File "env/lib/python3.8/site-packages/flask_accepts/utils.py", line 63, in for_swagger
fields = {
File "env/lib/python3.8/site-packages/flask_accepts/utils.py", line 64, in
k: map_type(v, api, model_name, operation)
File "env/lib/python3.8/site-packages/flask_accepts/utils.py", line 182, in map_type
return type_map[value_type](val, api, model_name, operation)
File "env/lib/python3.8/site-packages/flask_accepts/utils.py", line 19, in unpack_nested
model_name = get_default_model_name(val.nested)
File "env/lib/python3.8/site-packages/flask_accepts/utils.py", line 152, in get_default_model_name
return "".join(schema.name.rsplit("Schema", 1))

AttributeError: 'str' object has no attribute 'name'

A change will also be necessary for the method map_type() since you have to give the object iteself while it's not yet charged. Can you please add the possibility to support this configuration ? Thank you

froggylab commented 4 years ago

In order to help, you can find attached the basic configuration I am using : case-accepts.tar.gz

thank you,

v1shwa commented 3 years ago

@froggylab Were you able to find a solution/workaround for this?

froggylab commented 3 years ago

@v1shwa The only two possibilities I found out were :

v1shwa commented 3 years ago

Thanks for quick response @froggylab . I don't think asking frontend to make another call is an option. I am planning to look into the source & see if we can tweak something. Hopefully, I will raise a PR for this soon.

ottj3 commented 3 years ago

Just ran into this myself, for anyone interested here's my workaround, exploiting the fact that marshmallow calculates these string-name-defined schema lazily and stores the result on the schema attribute of the field:

def unpack_nested(val, api, model_name: str = None, operation: str = "dump"):
    if val.nested == "self":
        return unpack_nested_self(val, api, model_name, operation)

+    if isinstance(val.nested, str):
+        parent = val.parent.parent if is_list_field(val.parent) else val.parent
+        parent_name = get_default_model_name(parent)
+        model_name = val.nested + '-in-' + parent_name
+        nested_schema = val.schema
+    else:
        model_name = get_default_model_name(val.nested)
+        nested_schema = val.nested

    if val.many:
        return fr.List(
            fr.Nested(
-                map_type(val.nested, api, model_name, operation), **_ma_field_to_fr_field(val)
+                map_type(nested_schema, api, model_name, operation), **_ma_field_to_fr_field(val)
        )
    )

    return fr.Nested(
-        map_type(val.nested, api, model_name, operation), **_ma_field_to_fr_field(val)
+        map_type(nested_schema, api, model_name, operation), **_ma_field_to_fr_field(val)
    )

I additionally change the model name to <child>-in-<parent>(-load/dump) to prevent conflicts in case your nested fields use only/include/exclude and thus have different fields in their schema. (for_swagger also has to be modified to deal with this, simply add conditions in here: https://github.com/apryor6/flask_accepts/blob/master/flask_accepts/utils.py#L74)

This method can be monkey-patched in via utils.type_map.update({fr.Nested: unpack_nested}) if you want to define it yourself instead of editing the library. (for_swagger can be likewise monkey-patched by updating Schema/SchemaMeta in type_map)

This probably needs some cleanup before being PR-ready material (not even sure if this is a good approach), but it might be a usable workaround for anyone who needs it in the interim.

Bonus: hacky workaround for pluck fields by replacing the first half of that update_nested with

if isinstance(val.nested, str):
    nested_schema = val.schema
else:
    nested_schema = val.nested()

plucked_field = nested_schema.fields[val.field_name]

(and pass plucked_field into map_type instead of nested_schema)