I have a User and Role model with a many-to-many relationship
class User(BaseModel, TimestampableMixin):
    username = Column(String(MEDIUM_STRING_LENGTH), nullable=False, unique=True)
    roles = relationship('Role', secondary='user_roles', back_populates='users')
class Role(BaseModel, TimestampableMixin):
    label = Column(String(MEDIUM_STRING_LENGTH), nullable=False, unique=True)
    users = relationship('User', secondary='user_roles', back_populates='roles')
class UserRole(BaseModel, TimestampableMixin):
    user_id = Column(ForeignKey('users.id', ondelete=CASCADE, onupdate=CASCADE), nullable=False, index=True)
    role_id = Column(ForeignKey('roles.id', onupdate=CASCADE), nullable=False, index=True)
I then defined schemas for User to nest Roles.
class RoleSchema(BaseSchema):
    class Meta:
        model = models.Role
class UserSchema(BaseSchema):
    class Meta:
        model = models.User
    roles = fields.Nested('RoleSchema', many=True, exclude=['users'])
For serialization this is working great where a list of role objects are included in a user GET request. What also works is POSTing a user with new role objects embedded in the request. What I have not been able to figure out is how to POST/PUT a list of existing role ids rather than create new objects. For example, this request works:
{
    "username": "testuser12",
    "roles": [
        {
            "label": "newrole"
        }
    ]
}
Response:
{
  "createdTime": "2020-02-06T19:13:29Z",
  "id": 4,
  "modifiedTime": "2020-02-06T19:13:29Z",
  "roles": [
    {
      "createdTime": "2020-02-06T19:13:29Z",
      "id": 2,
      "label": "newrole",
      "modifiedTime": "2020-02-06T19:13:29Z"
    }
  ],
  "username": "testuser12"
}
But neither of these requests work:
{
    "username": "testuser13",
    "roles": [
        1
    ]
}
{
    "username": "testuser13",
    "roles": [
        {
            "id": 1
        }
    ]
}
I'm getting this response:
{
  "errors": {
    "error": [
      "Unprocessable Entity"
    ],
    "message": [
      "The request was well-formed but was unable to be followed due to semantic errors."
    ]
  }
}
I can tell I'm missing something in the schema to be able to ingest ids rather than objects, and I suspect I need to make use of dump_only/load_only and potentially a separate schema for PUT. But, I haven't been able to find an example anywhere online for this use case.
It may also be helpful to mention that I'm using flask-smorest for the request validation and schema argument ingestion.
@B_API.route('/user/<user_id>')
class UserByIdResource(MethodView):
    @B_API.response(schemas.UserSchema)
    def get(self, user_id):
        """
        Get a single user by id
        """
        return models.User.query.get(user_id)
    @B_API.arguments(schemas.UserSchema)
    @B_API.response(schemas.UserSchema)
    def put(self, updated_user, user_id):
        """
        Update fields of an existing user
        """
        models.User.query.get_or_404(user_id, description=f'User with id {user_id} not found')
        user = updated_user.update_with_db(user_id)
        return user
update_with_db looks like:
    def update_with_db(self, id: int):
        self.id = id
        DB.session.merge(self)
        DB.session.commit()
        return self.query.get(id)
Thanks for any assistance.
                        
I ended up resolving this by creating a separate PUT schema and using Pluck for role ids.
These schemas are implemented with the following resource, taking in PUT schema for input validation, and returning a standard User Schema to comply with the GET and POST responses. This seems to work well, with the only downside being having to define a separate schema.
I can now make a PUT request like
and get a response like
which is what I was aiming for.