FastAPI/Pydantic alias existing ORM field - python

I need to point Pydantic to a different attribute when serializing an ORM model. alias= doesn't seem to work as expected. In the example below I have an ORM object with both id and uuid attributes. I want to serialize uuid as id.
The API response should be:
{
"id": "12345678-1234-5678-1234-567812345678",
"foo": "bar"
}
Full example:
from uuid import UUID
from fastapi import FastAPI
from pydantic import BaseModel, Field
from dataclasses import dataclass
class ApiSchema(BaseModel):
class Config:
orm_mode = True
uuid: UUID = Field(alias='id')
foo: str | None = None
#dataclass
class ORMModel:
id: int
uuid: UUID
foo: str = 'bar'
app = FastAPI()
#app.get("/")
def endpoint() -> ApiSchema:
t = ORMModel(id=1, uuid=UUID('12345678123456781234567812345678'), foo='bar')
return t
This raises
File fastapi/routing.py", line 141, in serialize_response
raise ValidationError(errors, field.type_)
pydantic.error_wrappers.ValidationError: 1 validation error for ApiSchema
response -> id
value is not a valid uuid (type=type_error.uuid)
The marshmallow equivalent of what I'm trying to achieve would be this:
import marshmallow as ma
class ApiSchema(ma.Schema):
id = ma.fields.UUID(attribute='uuid')
foo = ma.fields.Str()

You misunderstand how aliases work. An alias on a field takes priority (over the actual field name) when the fields are populated. That means, during initialization, the class will look for the alias of a field in the data it is supposed to parse.
The way you defined ApiSchema, the field uuid has the alias id. Therefore, when you are parsing an instance of ORMModel (happens in FastAPI behind the scenes via ApiSchema.from_orm), the ApiSchema class will look for an attribute named id on that ORMModel object to populate the uuid field.
Since your ORMModel actually has an attribute named id (with the value 1 in your example), its value is taken to be assigned to the uuid field of ApiSchema.
Obviously, the integer 1 is not a UUID object and can not be coerced into one, so you get that validation error telling you that the value it found for id is not a valid UUID.
Here is the problem boiled down to the essentials:
from uuid import UUID
from pydantic import BaseModel, Field, ValidationError
class ApiSchema(BaseModel):
uuid: UUID = Field(alias='id')
foo: str | None = None
try:
ApiSchema.parse_obj({"uuid": "this is ignored", "foo": "bar"})
except ValidationError as exc:
print(exc.json(indent=2))
try:
ApiSchema.parse_obj({"id": 1, "foo": "bar"})
except ValidationError as exc:
print(exc.json(indent=2))
The output of the first attempt:
[
{
"loc": [
"id"
],
"msg": "field required",
"type": "value_error.missing"
}
]
The second:
[
{
"loc": [
"id"
],
"msg": "value is not a valid uuid",
"type": "type_error.uuid"
}
]
I think you want it the other way around. I assume that your actual goal is to have a field named id on your ApiSchema model (and have that appear in your API endpoint) and alias it with uuid, so that it takes the value of the ORMModel.uuid attribute during initialization:
from uuid import UUID
from pydantic import BaseModel, Field
class ApiSchema(BaseModel):
id: UUID = Field(alias="uuid")
foo: str | None = None
obj = ApiSchema.parse_obj(
{
"id": "this is ignored",
"uuid": UUID("12345678123456781234567812345678"),
"foo": "bar",
}
)
print(obj.json(indent=2))
The output:
{
"id": "12345678-1234-5678-1234-567812345678",
"foo": "bar"
}
To fix your FastAPI example, you would therefore probably do this:
from dataclasses import dataclass
from uuid import UUID
from fastapi import FastAPI
from pydantic import BaseModel, Field
class ApiSchema(BaseModel):
id: UUID = Field(alias="uuid")
foo: str | None = None
class Config:
orm_mode = True
#dataclass
class ORMModel:
id: int
uuid: UUID
foo: str = "bar"
app = FastAPI()
#app.get("/", response_model=ApiSchema, response_model_by_alias=False)
def endpoint() -> ORMModel:
t = ORMModel(id=1, uuid=UUID("12345678123456781234567812345678"), foo="bar")
return t
Side note: Yes, the actual return type of endpoint is ORMModel. The wrapper returned by the decorator then takes that and turns it into an instance of ApiSchema via from_orm.
PS
Forgot the last part to actually get the response you want. You need to set response_model_by_alias=False in the route decorator (it is True by default) for the response to actually use the regular field name instead of the alias. I fixed the last code snipped accordingly. Now the response will be:
{"id":"12345678-1234-5678-1234-567812345678","foo":"bar"}
In the Pydantic BaseModel.json method the by_alias parameter has the value False by default. FastAPI does this differently.

Related

How to get value or name of enum from SQLAlchemy result query?

I want to build an API for my project and return everything as JSON using Flask and SQLAlchemy. Unfortunately, SQLAlchemy did not return the query as JSON Seriazeble, so I'm using data classes to solve that problem. The code working and it returns JSON as I wanted. The problem occurs when I try to implement enum column like gender, because the enum column returns an enum object, so it's not JSON Seriazeble again. This is the code:
ActivityLevel.py
class GenderEnum(Enum):
p = 0
l = 1
#dataclass
class ActivityLevel(db.Model):
__tablename__ = "activity_level"
id: int
name: str
gender: GenderEnum
activity_score: float
date_created: str
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
gender = db.Column(db.Enum(GenderEnum), nullable=False)
activity_score = db.Column(
db.Float(precision=3, decimal_return_scale=2), nullable=False
)
date_created = db.Column(db.DateTime, default=datetime.utcnow)
ActivityLevelController.py
from flask import jsonify
from flask_restful import Resource
from models.ActivityLevel import ActivityLevel
class ActivityLevelController(Resource):
def get(self):
try:
activity = ActivityLevel().query.all()
result = {
"activity": activity
}
print(activity)
return jsonify(result)
except Exception as e:
print(e)
return jsonify({"message": "Error again"})
And this is the result of print(activity)
[
ActivityLevel(id=1, name='asdfasdf', gender=<GenderEnum.p: 0>, activity_score=12.0, date_created=datetime.datetime(2022, 8, 12, 10, 54, 58)),
ActivityLevel(id=2, name='qwerqwer', gender=<GenderEnum.l: 1>, activity_score=13.0, date_created=datetime.datetime(2022, 8, 12, 10, 54, 58))
]
As you can see, gender did not return l or p, and it return <GenderEnum.l: 1>. Which is correct as the documentation says https://docs.python.org/3/library/enum.html, when i call GenderEnum.l it will result just like the response.
What I want to ask is something like this:
Is there a way to override the return value of GenderEnum.l to the value or name by doing something in GenderEnum class?
Is there a way I can get the value or name of GenderEnum when I query the data from the database?
Or a way to make the query call the gender value or name as the default instead the enum object?
Thank you very much.
Is there a way to override the return value of GenderEnum.l to the value or name by doing something in GenderEnum class?
That's not really what Enums are meant to do, but you can access the name and value of your enum via the name and value attributes. In fact, SQLAlchemy will store the name of the Enum in the database in most cases.
Is there a way I can get the value or name of GenderEnum when I query the data from the database?
You define your model with an gender as an enum, so when loading, that is what you will get.
Or a way to make the query call the gender value or name as the default instead the enum object?
Same answer as above, you define gender as an enum, so SQLAlchemy gives it to you.
But to fix your problem, I can only recommend you look into dependency inversion.
Have a model which corresponds to your logic and allows you to solve the problem, then this model can be mapped to an entity, which allows you to store the model in whatever persistance layer, and finally a converter/serialiser for your model to load from/dump to your API.
This way, your model is not tied to where you store it or how you recieve it.
Slightly simplified example from your code:
# model.py
from dataclasses import dataclass, field
from enum import Enum, auto
class GenderEnum(Enum):
p = auto()
l = auto()
#dataclass
class ActivityLevel:
id: int = field(init=False) # NOTE add `repr=False` if trying to display instances
name: str
gender: GenderEnum
# orm.py
from sqlalchemy import Column, Enum, ForeignKey, Integer, String, Table
from sqlalchemy.orm import registry
from .model import ActivityLevel
mapper_registry = registry()
activity_level = Table(
"activity_level",
mapper_registry.metadata,
Column("id", Integer, primary_key=True),
Column("name", String(100), nullable=False),
Column("gender", Enum(GenderEnum), nullable=False),
)
def start_mapper():
mapper_registry.map_imperatively(ActivityLevel, activity_level)
# serialiser.py
from json import JSONDecoder, JSONEncoder
from .model import ActivityLevel, GenderEnum
class ActivityLevelJSONEncoder(JSONEncoder):
def default(self, o):
try: # NOTE duck typing and asking for forgiveness not permission
return {
"name": o.name,
"gender": o.gender.name,
}
except AttributeError:
return super().default(self, o)
class ActivityLevelJSONDecoder(JSONDecoder):
def __init__(self, *args, **kwargs):
super().__init__(*args, object_hook=self._object_hook, **kwargs)
#staticmethod
def _object_hook(d: dict) -> ActivityLevel:
return ActivityLevel(
name=d["name"],
gender=GenderEnum[d["gender"]],
)
Then in during application startup, start the mappers, and whenever needed json.dumps your ActivityLevel instances with the kwarg cls=ActivityLevelJSONEncoder.
This is what I found to temporarily fix my problem. I found this post and answer and follow the answer of #Vlad Bezden that uses the f-string method to override the return value of enum object when I call it like this GenderEnum.l from this <GenderEnum.l: 1> to just the value like 0 or 1. This is what I change in the enum class
class GenderEnum(int, Enum):
p = 0
l = 1
I add additional int. From what I noticed in the query behavior, the query just essentially calls enum object like this GenderEnum.l so that's why I got the response like that, just like what ljmc's says in the comment of his/her answer. That code initially overrides the response to the value of the enum object. If you want to return the name of the enum object, I'm terribly sorry, I can't seem to find a way to accomplish that.
After that it change the result to this
{
"activity": [
{
"date_created": "Fri, 12 Aug 2022 17:22:03 GMT",
"gender": 0,
"id": 1,
"name": "asdf",
"activity_score": 12.0
},
],
}
It works and I can return it as JSON. In contrast to that, I really not recommended this way to solve this problem, cause I find a hard way to do some relationship calls like join, and I really recommend anyone to follow Ijmc's answer to solving this problem, that's why I gonna accept Ijmc's answer as the correct one.
Thank you very much for helping me Ijmc for giving me insight, so I can come up with this little solution.
In any case of you guys wondering example how to do a join so i can return it as json, link to this post

Getting nested (joined) tables to display in the OpenAPI interface provided by FastAPI and SQLModel

I'm having trouble understanding how to display the children data in a one-to-many relationship using FastAPI and SQLModel. I'm using Python 3.10.3, FastAPI version 0.78.0 and SQLModel version 0.0.6. Here's a simplified version of the parent/child database models:
from datetime import datetime
from email.policy import default
from sqlalchemy import UniqueConstraint
from sqlmodel import Field, SQLModel, Relationship
class CustomerBase(SQLModel):
__table_args__ = (UniqueConstraint("email"),)
first_name: str
last_name: str
email: str
active: bool | None = True
class Customer(CustomerBase, table=True):
id: int | None =Field(primary_key=True, default=None)
class CustomerCreate(CustomerBase):
pass
class CustomerRead(CustomerBase):
id: int
class CustomerReadWithCalls(CustomerRead):
calls: list["CallRead"] = []
class CallBase(SQLModel):
duration: int
cost_per_minute: int | None = None
customer_id: int | None = Field(default=None, foreign_key="customer.id")
created: datetime = Field(nullable=False, default=datetime.now().date())
class Call(CallBase, table=True):
id: int | None = Field(primary_key=True)
class CallCreate(CallBase):
pass
class CallRead(CallBase):
id: int
class CallReadWithCustomer(CallRead):
customer: CustomerRead | None
Here is the API Route:
from fastapi import APIRouter, HTTPException, Depends, Query
from rbi_app.crud.customer import (
get_customers,
get_customer,
)
from rbi_app.models import (
CustomerRead,
CustomerReadWithCalls,
)
from rbi_app.database import Session, get_session
router = APIRouter()
#router.get("/customers/", status_code=200, response_model=list[CustomerRead])
def read_customers(
email: str = "",
offset: int = 0,
limit: int = Query(default=100, lte=100),
db: Session = Depends(get_session)
):
return get_customers(db, email, offset=offset, limit=limit)
#router.get("/customers/{customer_id}", status_code=200, response_model=CustomerReadWithCalls)
def read_customer(id: int, db: Session = Depends(get_session)):
customer = get_customer(db, id)
if customer is None:
raise HTTPException(status_code=404, detail=f"Customer not found for {id=}")
return customer
And here are the queries to the database the API Route endpoints make:
from sqlmodel import select
from rbi_app.database import Session
from rbi_app.models import (
Customer,
CustomerCreate,
)
# from rbi_app.schemas.customer import CustomerCreate
def get_customer(db: Session, id: int):
return db.get(Customer, id)
def get_customers(db: Session, email: str = "", offset: int = 0, limit: int = 100):
if email:
return db.exec(select(Customer).where(Customer.email == email)).first()
return db.exec(select(Customer).offset(offset).limit(limit).order_by(Customer.id)).all()
When I navigate to a route to get all a customer my query runs and I get a customer, but there is no "calls" list attribute in the customer. The OpenAPI display shows a "calls" attribute, but it's empty.
What am I doing wrong?
The issue here seems to be that you did not define the relationship on the Customer model (or the Call module). Since you query the database with the Customer model and it has no calls attribute, none of that data is present in the object returned by your get_customer function.
Even though your route defines the CustomerReadWithCalls as a response model, upon calling it, the object of that class can only ever be instantiated with the data returned by your route handler function, which is your Customer instance in this case. Since that does not even have the calls attribute (let alone the data), the CustomerReadWithCalls object is essentially created with the default value that you defined for the calls field -- the empty list.
Adding
calls: list["Call"] = Relationship(back_populates="customer")
to your Customer model should be enough.
(But as a side note, for me the route documentation only works properly, when I explicitly update the references on the CustomerReadWithCalls model after the CallRead definition.)
Here is a full working example.
models.py
from datetime import datetime
from sqlalchemy import UniqueConstraint
from sqlmodel import Field, Relationship, SQLModel
class CustomerBase(SQLModel):
__table_args__ = (UniqueConstraint("email"),)
first_name: str
last_name: str
email: str
active: bool | None = True
class Customer(CustomerBase, table=True):
id: int | None = Field(primary_key=True, default=None)
calls: list["Call"] = Relationship(back_populates="customer")
class CustomerCreate(CustomerBase):
pass
class CustomerRead(CustomerBase):
id: int
class CustomerReadWithCalls(CustomerRead):
calls: list["CallRead"] = []
class CallBase(SQLModel):
duration: int
cost_per_minute: int | None = None
customer_id: int | None = Field(default=None, foreign_key="customer.id")
created: datetime = Field(nullable=False, default=datetime.now().date())
class Call(CallBase, table=True):
id: int | None = Field(primary_key=True, default=None)
customer: Customer | None = Relationship(back_populates="calls")
class CallCreate(CallBase):
pass
class CallRead(CallBase):
id: int
# After the definition of `CallRead`, update the forward reference to it:
CustomerReadWithCalls.update_forward_refs()
class CallReadWithCustomer(CallRead):
customer: CustomerRead | None
routes.py
from fastapi import FastAPI, HTTPException, Depends
from sqlmodel import Session, SQLModel, create_engine
from .models import CustomerReadWithCalls, Customer, Call
api = FastAPI()
sqlite_file_name = 'database.db'
sqlite_url = f'sqlite:///{sqlite_file_name}'
engine = create_engine(sqlite_url, echo=True)
#api.on_event('startup')
def initialize_db():
SQLModel.metadata.drop_all(engine)
SQLModel.metadata.create_all(engine)
# For testing:
with Session(engine) as session:
customer = Customer(first_name="Foo", last_name="Bar", email="foo#bar.com")
call1 = Call(duration=123)
call2 = Call(duration=456)
customer.calls.extend([call1, call2])
session.add(customer)
session.commit()
def get_session() -> Session:
session = Session(engine)
try:
yield session
finally:
session.close()
def get_customer(db: Session, id: int):
return db.get(Customer, id)
#api.get("/customers/{customer_id}", status_code=200, response_model=CustomerReadWithCalls)
def read_customer(customer_id: int, db: Session = Depends(get_session)):
customer = get_customer(db, customer_id)
if customer is None:
raise HTTPException(status_code=404, detail=f"Customer not found for {customer_id=}")
return customer
Starting the API server and sending GET to http://127.0.0.1:8000/customers/1 gives me
{
"first_name": "Foo",
"last_name": "Bar",
"email": "foo#bar.com",
"active": true,
"id": 1,
"calls": [
{
"duration": 123,
"cost_per_minute": null,
"customer_id": 1,
"created": "2022-08-16T00:00:00",
"id": 1
},
{
"duration": 456,
"cost_per_minute": null,
"customer_id": 1,
"created": "2022-08-16T00:00:00",
"id": 2
}
]
}
Hope this helps.

Pydantic validations for extra fields that not defined in schema

I am using pydantic for schema validations and I would like to throw an error when any extra field is added to a schema that isn't defined.
from typing import Literal, Union
from pydantic import BaseModel, Field, ValidationError
class Cat(BaseModel):
pet_type: Literal['cat']
meows: int
class Dog(BaseModel):
pet_type: Literal['dog']
barks: float
class Lizard(BaseModel):
pet_type: Literal['reptile', 'lizard']
scales: bool
class Model(BaseModel):
pet: Union[Cat, Dog, Lizard] = Field(..., discriminator='pet_type')
n: int
print(Model(pet={'pet_type': 'dog', 'barks': 3.14, 'eats': 'biscuit'}, n=1))
""" try:
Model(pet={'pet_type': 'dog'}, n=1)
except ValidationError as e:
print(e) """
In the above code, I have added the eats field which is not defined. The pydantic validations are applied and the extra values that I defined are removed in response. I wanna throw an error saying eats is not allowed for Dog or something like that. Is there any way to achieve that?
And is there any chance that we can provide the input directly instead of the pet object?
print(Model({'pet_type': 'dog', 'barks': 3.14, 'eats': 'biscuit', n=1})). I tried without descriminator but those specific validations are missing related to pet_type. Can someone guide me how to achive either one of that?
You can use the extra field in the Config class to forbid extra attributes during model initialisation (by default, additional attributes will be ignored).
For example:
from pydantic import BaseModel, Extra
class Pet(BaseModel):
name: str
class Config:
extra = Extra.forbid
data = {
"name": "some name",
"some_extra_field": "some value",
}
my_pet = Pet.parse_obj(data) # <- effectively the same as Pet(**pet_data)
will raise a VaidationError:
ValidationError: 1 validation error for Pet
some_extra_field
extra fields not permitted (type=value_error.extra)
Works as well when the model is "nested", e.g.:
class PetModel(BaseModel):
my_pet: Pet
n: int
pet_data = {
"my_pet": {"name": "Some Name", "invalid_field": "some value"},
"n": 5,
}
pet_model = PetModel.parse_obj(pet_data)
# Effectively the same as
# pet_model = PetModel(my_pet={"name": "Some Name", "invalid_field": "some value"}, n=5)
will raise:
ValidationError: 1 validation error for PetModel
my_pet -> invalid_field
extra fields not permitted (type=value_error.extra)
Pydantic is made to validate your input with the schema. In your case, you want to remove one of its validation feature.
I think you should create a new class that inherit from BaseModel
class ModifiedBaseModel(BaseModel):
def __init__(__pydantic_self__, **data: Any) -> None:
registered, not_registered = __pydantic_self__.filter_data(data)
super().__init__(**registered)
for k, v in not_registered.items():
__pydantic_self__.__dict__[k] = v
#classmethod
def filter_data(cls, data):
registered_attr = {}
not_registered_attr = {}
annots = cls.__annotations__
for k, v in data.items():
if k in annots:
registered_attr[k] = v
else:
not_registered_attr[k] = v
return registered_attr, not_registered_attr
then create your validation classes
class Cat(ModifiedBaseModel):
pet_type: Literal['cat']
meows: int
now you can create a new Cat without worries about undefined attribute. Like this
my_cat = Cat(pet_type='cat', meows=3, name='blacky', age=3)
2nd question, to put the input directly from dict you can use double asterisk **
Dog(**my_dog_data_in_dict)
or
Dog(**{'pet_type': 'dog', 'barks': 3.14, 'eats': 'biscuit', n=1})

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

Enums in Django and Graphene always result in null

I am having some issues trying to use enums with Django and Graphene. Even though the values of the enum are persisted in the SQLite and are retrieved properly, it keeps resulting in an error. Below is a sample of the error.
{
"message": "Cannot return null for non-nullable field DjangoObject.vanillaEnum.",
"locations": [
{
"line": 10,
"column": 5
}
]
},
I'm using Django 2.0.3 and Graphene 2.0.1 with Anaconda3 5.0.0 and Python 3.6.4. I managed to reproduce the error using a trivial example, which is available on my GitHub account.
In the models.py, I defined a Python Enum and a Django model that uses that enum. The models here work as intended (AFAIK) without involving any Graphene dependencies. The default SQLite database also appears to have the correct values.
models.py
import enum
from django.db import models
#enum.unique
class VanillaEnum(enum.Enum):
RED = enum.auto()
BLUE = enum.auto()
GREEN = enum.auto()
#classmethod
def choices(cls):
return tuple((x, x) for x in cls)
class DjangoModel(models.Model):
name = models.CharField(max_length=20)
vanilla_enum = models.CharField(choices=VanillaEnum.choices(), default=VanillaEnum.GREEN, max_length=20)
def __str__(self):
return f'name={self.name}, vanilla_enun={self.vanilla_enum}'
Next, in the schema.py, I defined the two enums. One uses the graphene.Enum.from_enum to convert the VanillaEnum into one that supposedly Graphene can use. The second one is an enum using graphene.Enum. The second enum is my control case, which should work. But it still results in the same error as above.
Then I defined two objects, a native Graphene object that uses both enums as fields, and a Django-Graphene object that is mapped to the model.
schema.py
import graphene
from graphene_django import DjangoObjectType
from djangographeneenum.models import DjangoModel, VanillaEnum
ConvertedEnum = graphene.Enum.from_enum(VanillaEnum)
class GrapheneEnum(graphene.Enum):
RED = 1
BLUE = 2
GREEN = 3
class GrapheneObject(graphene.ObjectType):
name = graphene.String()
converted_enum = graphene.Field(ConvertedEnum)
graphene_enum = graphene.Field(GrapheneEnum)
def __str__(self):
return f'name={self.name}, converted_enum={self.converted_enum}, graphene_enum={self.graphene_enum}'
class DjangoObject(DjangoObjectType):
class Meta:
model = DjangoModel
class Query(graphene.ObjectType):
graphene_object = graphene.Field(GrapheneObject)
django_model = graphene.List(DjangoObject)
def resolve_graphene_object(self, info):
graphene_object = GrapheneObject(name='Abc', converted_enum=ConvertedEnum.RED, graphene_enum=GrapheneEnum.BLUE)
print(f'graphene_object: {graphene_object}')
return graphene_object
def resolve_django_model(self, info):
django_model = DjangoModel.objects.get_or_create(name='RED Model', vanilla_enum=VanillaEnum.RED)
print(f'django_model: {django_model}')
django_model = DjangoModel.objects.get_or_create(name='BLUE Model', vanilla_enum=VanillaEnum.BLUE)
print(f'django_model: {django_model}')
django_model = DjangoModel.objects.get_or_create(name='GREEN Model', vanilla_enum=VanillaEnum.GREEN)
print(f'django_model: {django_model}')
objects_all = DjangoModel.objects.all()
for x in objects_all:
print(f'django_model: {x}')
return objects_all
Below is a partial output. The enums in the native Graphene object both show up as null. And for the mapped Django model, the entire object is null and not just the enum field.
"data": {
"grapheneObject": {
"name": "Abc",
"convertedEnum": null,
"grapheneEnum": null
},
"djangoModel": [
null,
null,
null,
null
]
}
So, what am I missing?

Categories

Resources