Add a custom filed to fastapi responce model (serializers) - python

I'm following this guide from the Fastapi documentation and I have a question what if want to add a custom field when I return an object from DB. In Django I can use serializers.
My case:
I want to save an image name into DB, but before that I need save an actual file in a static folder. When I call GET /items/1 I want to return not just an image name from DB, but full URL, so I need to execute some logic on every request in order to build the URL. The question is how can I achieve that? The only thing I can think of is to add an additional DTO layer that coverts input data to Pydantic classes, so it's gonna be:
DTO class -> Pydantic -> DB
Is there more fancy way of doing that?
Code example:
schemas.py
from typing import List, Literal, Optional
from enum import Enum, IntEnum
from pydantic import BaseModel, constr, validator
class Ingredient(BaseModel):
quantity: int
quantityUnit: QuantityUnitEnum
name: constr(max_length=50)
class RecipeBase(BaseModel):
id: int = None
title: constr(max_length=50)
# image_name: str
#validator('ingredients')
def ingredients_must_have_unique_name(cls, values):
names = []
for item in values:
names.append(item.name)
if len(names) > len(set(names)):
raise ValueError('must contain unique names')
return values
class RecipeCreate(RecipeBase):
pass
class Recipe(RecipeBase):
id: int
class Config:
orm_mode = True
model.py
class Recipe(Base):
__tablename__ = "recipes"
id = Column(Integer, primary_key=True, index=True)
title = Column(String(50), index=True, nullable=False)
image_name = Column(String(50), index=True, nullable=False)
main.py
#app.post("/recipes", response_model=schemas.Recipe)
def create_recipe(recipe: schemas.RecipeCreate, db: Session = Depends(get_db)):
return repository.create_recipe(db=db, recipe=recipe)
#app.get("/recipes/{recipe_id}", response_model=schemas.Recipe)
def get_recipe(recipe_id, db: Session = Depends(get_db)):
return repository.get_recipe(db, recipe_id=recipe_id)
repository.py
def get_recipe(db: Session, recipe_id: int):
return db.query(models.Recipe).get(recipe_id)

Related

SQLAlchemy: Is there a way to return the Column(comment=) instead of the name of the column in an API response?

Assume a simple schema defined as:
# Base -> class which allows inherit of declarative_base
class Monkey(Base):
__tablename__ = "monkey"
monkey_id = Column(BIGINT(20), primary_key=True, nullable=False, comment="PowerMonkey")
Lets then assume we have a simple GET API request:
# pseudocode
#app.get("/{monkey_id}")
async def get_monkey():
returns the data row where monkey has searched id in request
Is there a way to get:
{
"PowerMonkey" : 1
}
Instead of
{
"monkey_id" : 1
}
as the response body?
You can access the comment via the mapper, using the class:
from sqlalchemy import inspect
...
mapper = inspect(Monkey)
comment = mapper.columns['monkey_id'].comment
or an instance of the class:
mapper = inspect(Monkey()).mapper
comment = mapper.columns['monkey_id'].comment
However it's up to you to construct an API response using the comment's value, or to configure tools like Marshmallow or Pydantic to do so. Here's how you might do it using Marshmallow:
from marshmallow_sqlalchemy import SQLAlchemySchema
from marshmallow import fields
class MarshmallowMonkeySchema(SQLAlchemySchema):
class Meta:
model = Monkey
monkey_id = fields.Method('get_id')
def get_id(self, obj):
return sa.inspect(obj).mapper.columns['monkey_id'].comment
with Session() as s:
q = sa.select(Monkey)
m = s.scalars(q).first()
response = MarshmallowMonkeySchema().dump(m)
print(response)

Best way to flatten and remap ORM to Pydantic Model

I am using Pydantic with FastApi to output ORM data into JSON. I would like to flatten and remap the ORM model to eliminate an unnecessary level in the JSON.
Here's a simplified example to illustrate the problem.
original output: {"id": 1, "billing":
[
{"id": 1, "order_id": 1, "first_name": "foo"},
{"id": 2, "order_id": 1, "first_name": "bar"}
]
}
desired output: {"id": 1, "name": ["foo", "bar"]}
How to map values from nested dict to Pydantic Model? provides a solution that works for dictionaries by using the init function in the Pydantic model class. This example shows how that works with dictionaries:
from pydantic import BaseModel
# The following approach works with a dictionary as the input
order_dict = {"id": 1, "billing": {"first_name": "foo"}}
# desired output: {"id": 1, "name": "foo"}
class Order_Model_For_Dict(BaseModel):
id: int
name: str = None
class Config:
orm_mode = True
def __init__(self, **kwargs):
print(
"kwargs for dictionary:", kwargs
) # kwargs for dictionary: {'id': 1, 'billing': {'first_name': 'foo'}}
kwargs["name"] = kwargs["billing"]["first_name"]
super().__init__(**kwargs)
print(Order_Model_For_Dict.parse_obj(order_dict)) # id=1 name='foo'
(This script is complete, it should run "as is")
However, when working with ORM objects, this approach does not work. It appears that the init function is not called. Here's an example which will not provide the desired output.
from pydantic import BaseModel, root_validator
from typing import List
from sqlalchemy.orm import relationship
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.dialects.postgresql import ARRAY
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
from pydantic.utils import GetterDict
class BillingOrm(Base):
__tablename__ = "billing"
id = Column(Integer, primary_key=True, nullable=False)
order_id = Column(ForeignKey("orders.id", ondelete="CASCADE"), nullable=False)
first_name = Column(String(20))
class OrderOrm(Base):
__tablename__ = "orders"
id = Column(Integer, primary_key=True, nullable=False)
billing = relationship("BillingOrm")
class Billing(BaseModel):
id: int
order_id: int
first_name: str
class Config:
orm_mode = True
class Order(BaseModel):
id: int
name: List[str] = None
# billing: List[Billing] # uncomment to verify the relationship is working
class Config:
orm_mode = True
def __init__(self, **kwargs):
# This __init__ function does not run when using from_orm to parse ORM object
print("kwargs for orm:", kwargs)
kwargs["name"] = kwargs["billing"]["first_name"]
super().__init__(**kwargs)
billing_orm_1 = BillingOrm(id=1, order_id=1, first_name="foo")
billing_orm_2 = BillingOrm(id=2, order_id=1, first_name="bar")
order_orm = OrderOrm(id=1)
order_orm.billing.append(billing_orm_1)
order_orm.billing.append(billing_orm_2)
order_model = Order.from_orm(order_orm)
# Output returns 'None' for name instead of ['foo','bar']
print(order_model) # id=1 name=None
(This script is complete, it should run "as is")
The output returns name=None instead of the desired list of names.
In the above example, I am using Order.from_orm to create the Pydantic model. This approach seems to be the same that is used by FastApi when specifying a response model. The desired solution should support use in the FastApi response model as shown in this example:
#router.get("/orders", response_model=List[schemas.Order])
async def list_orders(db: Session = Depends(get_db)):
return get_orders(db)
Update:
Regarding MatsLindh comment to try validators, I replaced the init function with a root validator, however, I'm unable to mutate the return values to include a new attribute. I suspect this issue is because it is a ORM object and not a true dictionary. The following code will extract the names and print them in the desired list. However, I can't see how to include this updated result in the model response:
#root_validator(pre=True)
def flatten(cls, values):
if isinstance(values, GetterDict):
names = [
billing_entry.first_name for billing_entry in values.get("billing")
]
print(names)
# values["name"] = names # error: 'GetterDict' object does not support item assignment
return values
I also found a couple other discussions on this problem that led me to try this approach:
https://github.com/samuelcolvin/pydantic/issues/717
https://gitmemory.com/issue/samuelcolvin/pydantic/821/744047672
What if you override the from_orm class method?
class Order(BaseModel):
id: int
name: List[str] = None
billing: List[Billing]
class Config:
orm_mode = True
#classmethod
def from_orm(cls, obj: Any) -> 'Order':
# `obj` is the orm model instance
if hasattr(obj, 'billing'):
obj.name = obj.billing.first_name
return super().from_orm(obj)
I really missed the handy Django REST Framework serializers while working with the FastAPI + Pydantic stack... So I wrangled with GetterDict to allow defining field getter function in the Pydantic model like this:
class User(FromORM):
fullname: str
class Config(FromORM.Config):
getter_dict = FieldGetter.bind(lambda: User)
#staticmethod
def get_fullname(obj: User) -> str:
return f'{obj.firstname} {obj.lastname}'
where the magic part FieldGetter is implemented as
from typing import Any, Callable, Optional, Type
from types import new_class
from pydantic import BaseModel
from pydantic.utils import GetterDict
class FieldGetter(GetterDict):
model_class_forward_ref: Optional[Callable] = None
model_class: Optional[Type[BaseModel]] = None
def __new__(cls, *args, **kwargs):
inst = super().__new__(cls)
if cls.model_class_forward_ref:
inst.model_class = cls.model_class_forward_ref()
return inst
#classmethod
def bind(cls, model_class_forward_ref: Callable):
sub_class = new_class(f'{cls.__name__}FieldGetter', (cls,))
sub_class.model_class_forward_ref = model_class_forward_ref
return sub_class
def get(self, key: str, default):
if hasattr(self._obj, key):
return super().get(key, default)
getter_fun_name = f'get_{key}'
if not (getter := getattr(self.model_class, getter_fun_name, None)):
raise AttributeError(f'no field getter function found for {key}')
return getter(self._obj)
class FromORM(BaseModel):
class Config:
orm_mode = True
getter_dict = FieldGetter

fastapi: mapping sqlalchemy database model to pydantic geojson feature

I just started playing with FastAPI, SQLAlchemy, Pydantic and I'm trying to build a simple API endpoint to return the rows in a postgis table as a geojson feature collection.
This is my sqlalchemy model:
class Poi(Base):
__tablename__ = 'poi'
id = Column(Integer, primary_key=True)
name = Column(Text, nullable=False)
type_id = Column(Integer)
geometry = Column(Geometry('POINT', 4326, from_text='ST_GeomFromEWKT'),
nullable=False)
Using geojson_pydantic the relevant pydantic models are:
from geojson_pydantic.features import Feature, FeatureCollection
from geojson_pydantic.geometries import Point
from typing import List
class PoiProperties(BaseModel):
name: str
type_id: int
class PoiFeature(Feature):
id: int
geometry: Point
properties: PoiProperties
class PoiCollection(FeatureCollection):
features: List[PoiFeature]
Desired Output:
Ideally I'd like to be able to retrieve and return the database records like so:
def get_pois(db: Session, skip: int = 0, limit: int = 100):
return db.query(Poi).offset(skip).limit(limit).all()
#app.get("/geojson", response_model=PoiCollection)
def read_geojson(skip: int = 0,
limit: int = 100,
db: Session = Depends(get_db)):
return get_pois(db, skip=skip, limit=limit)
Still I'm trying to figure out how to map the name and type_id columns from the db model to the PoiProperties in the PoiFeature object.
You want to return the PoiCollection schema (response_model=schemas.PoiCollection) except that you return your database response directly without any formatting. So you have to convert your crud response into your schema response.
# Different function for translate db response to Pydantic response according to your different schema
def make_response_poi_properties(poi):
return PoiFeature(name=poi.name, type_id=poi.type_id)
def make_response_poi_feature(poi):
return PoiFeature(id=poi.id, geometry=poi.geometry,properties=make_response_poi_properties(poi))
def make_response_poi_collection(pois):
response = []
for poi in pois:
response.append(make_response_poi_feature(poi)
return response
#app.get("/geojson", response_model=PoiCollection)
def read_geojson(skip: int = 0,
limit: int = 100,
db: Session = Depends(get_db)):
# Call function for translate db data to pydantic data according to your response_model
return make_response_poi_collection(get_pois(db, skip=skip, limit=limit))
or simply use the orm mode inside your different schema class

FastAPI - how to generate random ID?

I'm making simple CRUD API using FastAPI and what I want to do is generate unique random when creating new item (other fields are address and name which should be filled by user). How can I do that?
There is fragment of my code with class and a POST function.
app = FastAPI()
userdb = []
class User(BaseModel):
id: int
address: str
name: str
#app.post("/users")
def add_user(user: User):
userdb.append(users.dict())
return userdb[-1]
uuid4 is often the way to go
It'll be absolutely unique amongst any id ever generated with the function anywhere with astronomical likelihood (refer to RFC-4122 Section 4.4) and is very fast
from uuid import uuid4
...
unique_id = str(uuid4())
Another option - often what you want is for this ID to not necessarily be random, but rather to be some auto-incremented index from your database. The FastAPI docs give an example of this:
...
notes = sqlalchemy.Table(
"notes",
metadata,
sqlalchemy.Column("id", sqlalchemy.Integer, primary_key=True),
sqlalchemy.Column("text", sqlalchemy.String),
sqlalchemy.Column("completed", sqlalchemy.Boolean),
)
...
class NoteIn(BaseModel):
text: str
completed: bool
class Note(BaseModel):
id: int
text: str
completed: bool
...
#app.post("/notes/", response_model=Note)
async def create_note(note: NoteIn):
query = notes.insert().values(text=note.text, completed=note.completed)
last_record_id = await database.execute(query)
return {**note.dict(), "id": last_record_id}
https://fastapi.tiangolo.com/advanced/async-sql-databases/?h=id#about-notedict-id-last_record_id
In your case you would use separate models for UserIn and User. Then in your example you would then assign the ID in the response model as the index in your userdb list (which in a real app would probably not just be a list, but a database).
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
userdb = []
class UserIn(BaseModel):
address: str
name: str
class User(BaseModel):
id: int
address: str
name: str
#app.post("/users")
def add_user(user_in: UserIn) -> User:
userdb.append(user_in)
user_out_dict = userdb[-1].dict()
user_out_dict.update({"id": len(userdb)-1})
return User(**user_out_dict)

jsonify a SQLAlchemy result set in Flask [duplicate]

This question already has answers here:
How to serialize SqlAlchemy result to JSON?
(37 answers)
Closed 4 years ago.
I'm trying to jsonify a SQLAlchemy result set in Flask/Python.
The Flask mailing list suggested the following method http://librelist.com/browser//flask/2011/2/16/jsonify-sqlalchemy-pagination-collection-result/#04a0754b63387f87e59dda564bde426e :
return jsonify(json_list = qryresult)
However I'm getting the following error back:
TypeError: <flaskext.sqlalchemy.BaseQuery object at 0x102c2df90>
is not JSON serializable
What am I overlooking here?
I have found this question: How to serialize SqlAlchemy result to JSON? which seems very similar however I didn't know whether Flask had some magic to make it easier as the mailing list post suggested.
Edit: for clarification, this is what my model looks like
class Rating(db.Model):
__tablename__ = 'rating'
id = db.Column(db.Integer, primary_key=True)
fullurl = db.Column(db.String())
url = db.Column(db.String())
comments = db.Column(db.Text)
overall = db.Column(db.Integer)
shipping = db.Column(db.Integer)
cost = db.Column(db.Integer)
honesty = db.Column(db.Integer)
communication = db.Column(db.Integer)
name = db.Column(db.String())
ipaddr = db.Column(db.String())
date = db.Column(db.String())
def __init__(self, fullurl, url, comments, overall, shipping, cost, honesty, communication, name, ipaddr, date):
self.fullurl = fullurl
self.url = url
self.comments = comments
self.overall = overall
self.shipping = shipping
self.cost = cost
self.honesty = honesty
self.communication = communication
self.name = name
self.ipaddr = ipaddr
self.date = date
It seems that you actually haven't executed your query. Try following:
return jsonify(json_list = qryresult.all())
[Edit]: Problem with jsonify is, that usually the objects cannot be jsonified automatically. Even Python's datetime fails ;)
What I have done in the past, is adding an extra property (like serialize) to classes that need to be serialized.
def dump_datetime(value):
"""Deserialize datetime object into string form for JSON processing."""
if value is None:
return None
return [value.strftime("%Y-%m-%d"), value.strftime("%H:%M:%S")]
class Foo(db.Model):
# ... SQLAlchemy defs here..
def __init__(self, ...):
# self.foo = ...
pass
#property
def serialize(self):
"""Return object data in easily serializable format"""
return {
'id' : self.id,
'modified_at': dump_datetime(self.modified_at),
# This is an example how to deal with Many2Many relations
'many2many' : self.serialize_many2many
}
#property
def serialize_many2many(self):
"""
Return object's relations in easily serializable format.
NB! Calls many2many's serialize property.
"""
return [ item.serialize for item in self.many2many]
And now for views I can just do:
return jsonify(json_list=[i.serialize for i in qryresult.all()])
[Edit 2019]:
In case you have more complex objects or circular references, use a library like marshmallow).
Here's what's usually sufficient for me:
I create a serialization mixin which I use with my models. The serialization function basically fetches whatever attributes the SQLAlchemy inspector exposes and puts it in a dict.
from sqlalchemy.inspection import inspect
class Serializer(object):
def serialize(self):
return {c: getattr(self, c) for c in inspect(self).attrs.keys()}
#staticmethod
def serialize_list(l):
return [m.serialize() for m in l]
All that's needed now is to extend the SQLAlchemy model with the Serializer mixin class.
If there are fields you do not wish to expose, or that need special formatting, simply override the serialize() function in the model subclass.
class User(db.Model, Serializer):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String)
password = db.Column(db.String)
# ...
def serialize(self):
d = Serializer.serialize(self)
del d['password']
return d
In your controllers, all you have to do is to call the serialize() function (or serialize_list(l) if the query results in a list) on the results:
def get_user(id):
user = User.query.get(id)
return json.dumps(user.serialize())
def get_users():
users = User.query.all()
return json.dumps(User.serialize_list(users))
I had the same need, to serialize into json. Take a look at this question. It shows how to discover columns programmatically. So, from that I created the code below. It works for me, and I'll be using it in my web app. Happy coding!
def to_json(inst, cls):
"""
Jsonify the sql alchemy query result.
"""
convert = dict()
# add your coversions for things like datetime's
# and what-not that aren't serializable.
d = dict()
for c in cls.__table__.columns:
v = getattr(inst, c.name)
if c.type in convert.keys() and v is not None:
try:
d[c.name] = convert[c.type](v)
except:
d[c.name] = "Error: Failed to covert using ", str(convert[c.type])
elif v is None:
d[c.name] = str()
else:
d[c.name] = v
return json.dumps(d)
class Person(base):
__tablename__ = 'person'
id = Column(Integer, Sequence('person_id_seq'), primary_key=True)
first_name = Column(Text)
last_name = Column(Text)
email = Column(Text)
#property
def json(self):
return to_json(self, self.__class__)
Here's my approach:
https://github.com/n0nSmoker/SQLAlchemy-serializer
pip install SQLAlchemy-serializer
You can easily add mixin to your model and then just call
.to_dict() method on its instance.
You also can write your own mixin on base of SerializerMixin.
For a flat query (no joins) you can do this
#app.route('/results/')
def results():
data = Table.query.all()
result = [d.__dict__ for d in data]
return jsonify(result=result)
and if you only want to return certain columns from the database you can do this
#app.route('/results/')
def results():
cols = ['id', 'url', 'shipping']
data = Table.query.all()
result = [{col: getattr(d, col) for col in cols} for d in data]
return jsonify(result=result)
Ok, I've been working on this for a few hours, and I've developed what I believe to be the most pythonic solution yet. The following code snippets are python3 but shouldn't be too horribly painful to backport if you need.
The first thing we're gonna do is start with a mixin that makes your db models act kinda like dicts:
from sqlalchemy.inspection import inspect
class ModelMixin:
"""Provide dict-like interface to db.Model subclasses."""
def __getitem__(self, key):
"""Expose object attributes like dict values."""
return getattr(self, key)
def keys(self):
"""Identify what db columns we have."""
return inspect(self).attrs.keys()
Now we're going to define our model, inheriting the mixin:
class MyModel(db.Model, ModelMixin):
id = db.Column(db.Integer, primary_key=True)
foo = db.Column(...)
bar = db.Column(...)
# etc ...
That's all it takes to be able to pass an instance of MyModel() to dict() and get a real live dict instance out of it, which gets us quite a long way towards making jsonify() understand it. Next, we need to extend JSONEncoder to get us the rest of the way:
from flask.json import JSONEncoder
from contextlib import suppress
class MyJSONEncoder(JSONEncoder):
def default(self, obj):
# Optional: convert datetime objects to ISO format
with suppress(AttributeError):
return obj.isoformat()
return dict(obj)
app.json_encoder = MyJSONEncoder
Bonus points: if your model contains computed fields (that is, you want your JSON output to contain fields that aren't actually stored in the database), that's easy too. Just define your computed fields as #propertys, and extend the keys() method like so:
class MyModel(db.Model, ModelMixin):
id = db.Column(db.Integer, primary_key=True)
foo = db.Column(...)
bar = db.Column(...)
#property
def computed_field(self):
return 'this value did not come from the db'
def keys(self):
return super().keys() + ['computed_field']
Now it's trivial to jsonify:
#app.route('/whatever', methods=['GET'])
def whatever():
return jsonify(dict(results=MyModel.query.all()))
If you are using flask-restful you can use marshal:
from flask.ext.restful import Resource, fields, marshal
topic_fields = {
'title': fields.String,
'content': fields.String,
'uri': fields.Url('topic'),
'creator': fields.String,
'created': fields.DateTime(dt_format='rfc822')
}
class TopicListApi(Resource):
def get(self):
return {'topics': [marshal(topic, topic_fields) for topic in DbTopic.query.all()]}
You need to explicitly list what you are returning and what type it is, which I prefer anyway for an api. Serialization is easily taken care of (no need for jsonify), dates are also not a problem. Note that the content for the uri field is automatically generated based on the topic endpoint and the id.
Here's my answer if you're using the declarative base (with help from some of the answers already posted):
# in your models definition where you define and extend declarative_base()
from sqlalchemy.ext.declarative import declarative_base
...
Base = declarative_base()
Base.query = db_session.query_property()
...
# define a new class (call "Model" or whatever) with an as_dict() method defined
class Model():
def as_dict(self):
return { c.name: getattr(self, c.name) for c in self.__table__.columns }
# and extend both the Base and Model class in your model definition, e.g.
class Rating(Base, Model):
____tablename__ = 'rating'
id = db.Column(db.Integer, primary_key=True)
fullurl = db.Column(db.String())
url = db.Column(db.String())
comments = db.Column(db.Text)
...
# then after you query and have a resultset (rs) of ratings
rs = Rating.query.all()
# you can jsonify it with
s = json.dumps([r.as_dict() for r in rs], default=alchemyencoder)
print (s)
# or if you have a single row
r = Rating.query.first()
# you can jsonify it with
s = json.dumps(r.as_dict(), default=alchemyencoder)
# you will need this alchemyencoder where your are calling json.dumps to handle datetime and decimal format
# credit to Joonas # http://codeandlife.com/2014/12/07/sqlalchemy-results-to-json-the-easy-way/
def alchemyencoder(obj):
"""JSON encoder function for SQLAlchemy special classes."""
if isinstance(obj, datetime.date):
return obj.isoformat()
elif isinstance(obj, decimal.Decimal):
return float(obj)
Flask-Restful 0.3.6 the Request Parsing recommend marshmallow
marshmallow is an ORM/ODM/framework-agnostic library for converting
complex datatypes, such as objects, to and from native Python
datatypes.
A simple marshmallow example is showing below.
from marshmallow import Schema, fields
class UserSchema(Schema):
name = fields.Str()
email = fields.Email()
created_at = fields.DateTime()
from marshmallow import pprint
user = User(name="Monty", email="monty#python.org")
schema = UserSchema()
result = schema.dump(user)
pprint(result)
# {"name": "Monty",
# "email": "monty#python.org",
# "created_at": "2014-08-17T14:54:16.049594+00:00"}
The core features contain
Declaring Schemas
Serializing Objects (“Dumping”)
Deserializing Objects (“Loading”)
Handling Collections of Objects
Validation
Specifying Attribute Names
Specifying Serialization/Deserialization Keys
Refactoring: Implicit Field Creation
Ordering Output
“Read-only” and “Write-only” Fields
Specify Default Serialization/Deserialization Values
Nesting Schemas
Custom Fields
Here is a way to add an as_dict() method on every class, as well as any other method you want to have on every single class.
Not sure if this is the desired way or not, but it works...
class Base(object):
def as_dict(self):
return dict((c.name,
getattr(self, c.name))
for c in self.__table__.columns)
Base = declarative_base(cls=Base)
I've been looking at this problem for the better part of a day, and here's what I've come up with (credit to https://stackoverflow.com/a/5249214/196358 for pointing me in this direction).
(Note: I'm using flask-sqlalchemy, so my model declaration format is a bit different from straight sqlalchemy).
In my models.py file:
import json
class Serializer(object):
__public__ = None
"Must be implemented by implementors"
def to_serializable_dict(self):
dict = {}
for public_key in self.__public__:
value = getattr(self, public_key)
if value:
dict[public_key] = value
return dict
class SWEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, Serializer):
return obj.to_serializable_dict()
if isinstance(obj, (datetime)):
return obj.isoformat()
return json.JSONEncoder.default(self, obj)
def SWJsonify(*args, **kwargs):
return current_app.response_class(json.dumps(dict(*args, **kwargs), cls=SWEncoder, indent=None if request.is_xhr else 2), mimetype='application/json')
# stolen from https://github.com/mitsuhiko/flask/blob/master/flask/helpers.py
and all my model objects look like this:
class User(db.Model, Serializer):
__public__ = ['id','username']
... field definitions ...
In my views I call SWJsonify wherever I would have called Jsonify, like so:
#app.route('/posts')
def posts():
posts = Post.query.limit(PER_PAGE).all()
return SWJsonify({'posts':posts })
Seems to work pretty well. Even on relationships. I haven't gotten far with it, so YMMV, but so far it feels pretty "right" to me.
Suggestions welcome.
I was looking for something like the rails approach used in ActiveRecord to_json and implemented something similar using this Mixin after being unsatisfied with other suggestions. It handles nested models, and including or excluding attributes of the top level or nested models.
class Serializer(object):
def serialize(self, include={}, exclude=[], only=[]):
serialized = {}
for key in inspect(self).attrs.keys():
to_be_serialized = True
value = getattr(self, key)
if key in exclude or (only and key not in only):
to_be_serialized = False
elif isinstance(value, BaseQuery):
to_be_serialized = False
if key in include:
to_be_serialized = True
nested_params = include.get(key, {})
value = [i.serialize(**nested_params) for i in value]
if to_be_serialized:
serialized[key] = value
return serialized
Then, to get the BaseQuery serializable I extended BaseQuery
class SerializableBaseQuery(BaseQuery):
def serialize(self, include={}, exclude=[], only=[]):
return [m.serialize(include, exclude, only) for m in self]
For the following models
class ContactInfo(db.Model, Serializer):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
full_name = db.Column(db.String())
source = db.Column(db.String())
source_id = db.Column(db.String())
email_addresses = db.relationship('EmailAddress', backref='contact_info', lazy='dynamic')
phone_numbers = db.relationship('PhoneNumber', backref='contact_info', lazy='dynamic')
class EmailAddress(db.Model, Serializer):
id = db.Column(db.Integer, primary_key=True)
email_address = db.Column(db.String())
type = db.Column(db.String())
contact_info_id = db.Column(db.Integer, db.ForeignKey('contact_info.id'))
class PhoneNumber(db.Model, Serializer):
id = db.Column(db.Integer, primary_key=True)
phone_number = db.Column(db.String())
type = db.Column(db.String())
contact_info_id = db.Column(db.Integer, db.ForeignKey('contact_info.id'))
phone_numbers = db.relationship('Invite', backref='phone_number', lazy='dynamic')
You could do something like
#app.route("/contact/search", methods=['GET'])
def contact_search():
contact_name = request.args.get("name")
matching_contacts = ContactInfo.query.filter(ContactInfo.full_name.like("%{}%".format(contact_name)))
serialized_contact_info = matching_contacts.serialize(
include={
"phone_numbers" : {
"exclude" : ["contact_info", "contact_info_id"]
},
"email_addresses" : {
"exclude" : ["contact_info", "contact_info_id"]
}
}
)
return jsonify(serialized_contact_info)
I was working with a sql query defaultdict of lists of RowProxy objects named jobDict
It took me a while to figure out what Type the objects were.
This was a really simple quick way to resolve to some clean jsonEncoding just by typecasting the row to a list and by initially defining the dict with a value of list.
jobDict = defaultdict(list)
def set_default(obj):
# trickyness needed here via import to know type
if isinstance(obj, RowProxy):
return list(obj)
raise TypeError
jsonEncoded = json.dumps(jobDict, default=set_default)
I just want to add my method to do this.
just define a custome json encoder to serilize your db models.
class ParentEncoder(json.JSONEncoder):
def default(self, obj):
# convert object to a dict
d = {}
if isinstance(obj, Parent):
return {"id": obj.id, "name": obj.name, 'children': list(obj.child)}
if isinstance(obj, Child):
return {"id": obj.id, "name": obj.name}
d.update(obj.__dict__)
return d
then in your view function
parents = Parent.query.all()
dat = json.dumps({"data": parents}, cls=ParentEncoder)
resp = Response(response=dat, status=200, mimetype="application/json")
return (resp)
it works well though the parent have relationships
It's been a lot of times and there are lots of valid answers, but the following code block seems to work:
my_object = SqlAlchemyModel()
my_serializable_obj = my_object.__dict__
del my_serializable_obj["_sa_instance_state"]
print(jsonify(my_serializable_object))
I'm aware that this is not a perfect solution, nor as elegant as the others, however for those who want o quick fix, they might try this.

Categories

Resources