Repository: marshmallow-code/marshmallow-sqlalchemy

Issue:
Synonyms on Models
I have a model which makes use of a [synonym](https://docs.sqlalchemy.org/en/latest/orm/mapped_attributes.html#synonyms) for convenience. Flask.Marshmallow raises an `AttributeError` exception when a ModelSchema that uses this class is defined.

```
class Department(Model):
    """Represents a department."""

    MAXLEN_NAME = 255

    name = Column(
        String(MAXLEN_NAME),
        nullable=False,
        unique=True,
        index=True
    )
    department = synonym('name')


class DepartmentSchema(ma.ModelSchema):
    """Schema for Department."""

    class Meta:
        """Meta for Marshmallow-SQLAlchemy."""

        model = Department

```
This causes the following exception:

```
...
  File "devel/gesific/webapp/schemas.py", line 253, in <module>
    class DepartmentSchema(ma.ModelSchema):
  File ".virtualenvs/gesific/lib/python3.5/site-packages/marshmallow/schema.py", line 118, in __new__
    dict_cls=dict_cls
  File ".virtualenvs/gesific/lib/python3.5/site-packages/marshmallow_sqlalchemy/schema.py", line 60, in get_declared_fields
    declared_fields = mcs.get_fields(converter, opts, base_fields, dict_cls)
  File ".virtualenvs/gesific/lib/python3.5/site-packages/marshmallow_sqlalchemy/schema.py", line 94, in get_fields
    dict_cls=dict_cls,
  File ".virtualenvs/gesific/lib/python3.5/site-packages/marshmallow_sqlalchemy/convert.py", line 102, in fields_for_model
    field = base_fields.get(prop.key) or self.property2field(prop)
  File ".virtualenvs/gesific/lib/python3.5/site-packages/marshmallow_sqlalchemy/convert.py", line 124, in property2field
    field_class = field_class or self._get_field_class_for_property(prop)
  File ".virtualenvs/gesific/lib/python3.5/site-packages/marshmallow_sqlalchemy/convert.py", line 185, in _get_field_class_for_property
    column = prop.columns[0]
  File ".virtualenvs/gesific/lib/python3.5/site-packages/sqlalchemy/util/langhelpers.py", line 853, in __getattr__
    return self._fallback_getattr(key)
  File ".virtualenvs/gesific/lib/python3.5/site-packages/sqlalchemy/util/langhelpers.py", line 831, in _fallback_getattr
    raise AttributeError(key)
AttributeError: columns

```
Forcing the exclusion of the synonym solves the issue, but I think that it would be better if somehow Flask-Marshmallow were able to handle this automatically, either by duplicating the field or by ignoring the synonyms.

Discussion:

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 626bc52..e4d8793 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,6 +1,15 @@
 Changelog
 ---------
 
+0.22.2 (unreleased)
++++++++++++++++++++
+
+Bug fixes:
+
+* Avoid error when using ``SQLAlchemyModelSchema``, ``ModelSchema``, or ``fields_for_model``
+  with a model that has a ``SynonymProperty`` (:issue:`190`).
+* ``auto_field`` and ``field_for`` work with ``SynonymProperty``.
+
 0.22.1 (2020-02-09)
 +++++++++++++++++++
 
diff --git a/src/marshmallow_sqlalchemy/convert.py b/src/marshmallow_sqlalchemy/convert.py
index d3f0af2..32f581c 100644
--- a/src/marshmallow_sqlalchemy/convert.py
+++ b/src/marshmallow_sqlalchemy/convert.py
@@ -6,6 +6,7 @@ import uuid
 import marshmallow as ma
 from marshmallow import validate, fields
 from sqlalchemy.dialects import postgresql, mysql, mssql
+from sqlalchemy.orm import SynonymProperty
 import sqlalchemy as sa
 
 from .exceptions import ModelConversionError
@@ -93,6 +94,8 @@ class ModelConverter:
                 # Allow marshmallow to validate and exclude the field key.
                 result[prop.key] = None
                 continue
+            if isinstance(prop, SynonymProperty):
+                continue
             if hasattr(prop, "columns"):
                 if not include_fk:
                     # Only skip a column if there is no overriden column
@@ -134,6 +137,8 @@ class ModelConverter:
         return result
 
     def property2field(self, prop, *, instance=True, field_class=None, **kwargs):
+        if hasattr(prop, "_proxied_property"):  # handle synonyms
+            prop = prop._proxied_property
         field_class = field_class or self._get_field_class_for_property(prop)
         if not instance:
             return field_class
@@ -283,8 +288,8 @@ class ModelConverter:
 default_converter = ModelConverter()
 
 fields_for_model = default_converter.fields_for_model
-"""Generate a dict of field_name: `marshmallow.fields.Field` pairs for the
-given model.
+"""Generate a dict of field_name: `marshmallow.fields.Field` pairs for the given model.
+Note: SynonymProperties are ignored. Use an explicit field if you want to include a synonym.
 
 :param model: The SQLAlchemy model
 :param bool include_fk: Whether to include foreign key fields in the output.


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index c734cf8..f6a6fd3 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -5,7 +5,7 @@ import pytest
 import sqlalchemy as sa
 from sqlalchemy.ext.associationproxy import association_proxy
 from sqlalchemy.ext.declarative import declarative_base
-from sqlalchemy.orm import sessionmaker, relationship, backref, column_property
+from sqlalchemy.orm import sessionmaker, relationship, backref, column_property, synonym
 from marshmallow import validate
 
 
@@ -118,6 +118,7 @@ def models(Base):
             sa.Integer, sa.ForeignKey(School.id), nullable=True
         )
         current_school = relationship(School, backref=backref("teachers"))
+        curr_school_id = synonym("current_school_id")
 
         substitute = relationship("SubstituteTeacher", uselist=False, backref="teacher")
 
diff --git a/tests/test_sqlalchemy_schema.py b/tests/test_sqlalchemy_schema.py
index a4138ba..da4f827 100644
--- a/tests/test_sqlalchemy_schema.py
+++ b/tests/test_sqlalchemy_schema.py
@@ -219,6 +219,31 @@ def test_passing_table_to_auto_field(models, teacher):
     }
 
 
+# https://github.com/marshmallow-code/marshmallow-sqlalchemy/issues/190
+def test_auto_schema_skips_synonyms(models):
+    class TeacherSchema(SQLAlchemyAutoSchema):
+        class Meta:
+            model = models.Teacher
+            include_fk = True
+
+    schema = TeacherSchema()
+    assert "current_school_id" in schema.fields
+    assert "curr_school_id" not in schema.fields
+
+
+def test_auto_field_works_with_synonym(models):
+    class TeacherSchema(SQLAlchemyAutoSchema):
+        class Meta:
+            model = models.Teacher
+            include_fk = True
+
+        curr_school_id = auto_field()
+
+    schema = TeacherSchema()
+    assert "current_school_id" in schema.fields
+    assert "curr_school_id" in schema.fields
+
+
 class TestAliasing:
     @pytest.fixture
     def aliased_schema(self, models):