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?
Related
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.
I am looking for the good architecture for my problem. I am using django rest framework for building an API. I receive a list of dict which contains an id and a list of values. The list of values need to be validated according to the id.
Example of my code:
class AttributesSerializer(serializers.Serializer):
id = serializers.PrimaryKeyRelatedField(queryset=Attribute.objects.all(), source="attribute", required=True)
values = serializers.ListField()
def validate(self, validated_data):
attribute = validated_data["attribute"]
values = validated_data["values"]
# This function returns the corresponding field according to attribute
values_child_field = get_values_field(attribute)
self.fields["values"].child = values_child_fields
new_values = self.fields["values"].run_child_validation(values)
set_value(validated_data, "values", new_values)
return validated_data
class BaseObjectApiInputSerializer(serializers.Serializer):
category_id = serializers.PrimaryKeyRelatedField(
queryset=Category.objects.all()
)
attributes = AttributesSerializer(many=True)
I want to parse json like this:
{
"categorty_id": 42, # Category pk of the baseobject. which defines some constraints about attributes available
"attributes": [
{"id": 124, "values": ["value"]},
{"id": 321, "values": [42]},
{
"id": 18,
"values": [
{
"location": {"type": "Point", "geometry": {...}},
"address": "an address",
}
],
},
]
}
Currently, this code does not work. DRF seems to try to revalidate all values entries for each iteration with each child field. I do not understand why... I guess I could make it work without using this fields["values"] for making the validation and just retrieve the field and use it directly, but i need this field for making the save later.
Do you think my architecture is ok? What is the good way for parsing this type of data with DRF?
EDIT:
Structure of models are complex but a version simplified following:
class Attribute(models.Model):
class DataType(models.TextChoices):
TEXT = "TEXT", _("datatype_text")
INTEGER = "INTEGER", _("datatype_integer")
DATETIME = "DATETIME", _("datatype_datetime")
BOOL = "BOOL", _("datatype_bool")
# Some examples, but there are about 30 items with
# type very complicated like RecurrenceRule (RFC2445)
# or GeoJSON type
label = models.CharField()
category = models.ForeignKey(Category)
attribute_type = models.CharField(choices=DataType.choices)
class AttributeValue(models.Model):
attribute = models.ForeignKey(Attribute)
# a model which represents an object with list of attributes
baseobject = models.ForeignKey(BaseObject)
value = models.TextField()
AttributeValue is like a through table for manytomany relation between BaseObject model and Attribute model.
My JSON represents the list of attribute/values attached to a baseobject.
In fact I don't understand why DRf doesn't allow delegating registration in the child serializers of the parent serializer. This would allow much greater flexibility in code architecture and separation of responsibilities.
EDIT 2 :
My urls.py
router = routers.DefaultRouter()
router.register("baseobjects", BaseObjectViewSet, basename="baseobjects")
I am using the default router and url for DRF viewset.
The view looks like:
class BaseObjectViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated]
authentication_classes = [TokenAuthentication]
def create(self, request, *args, **kwargs):
serializer = BaseObjectApiInputSerializer(
data=request.data
)
if not serializer.is_valid():
return Response(serializer.errors, status=HTTP_400_BAD_REQUEST)
baseobject: BaseObject = serializer.save()
return Response(
{"results": [{"id": baseobject.pk}]}, status=HTTP_200_OK
)
I think you should use ListField with JSONField as child argument for values field.
validators = {
TinyurlShortener.DataType.TEXT: serializers.CharField(),
TinyurlShortener.DataType.INTEGER: serializers.IntegerField(),
TinyurlShortener.DataType.DATETIME: serializers.DateTimeField(),
TinyurlShortener.DataType.BOOL: serializers.BooleanField(),
}
class AttributesSerializer(serializers.Serializer):
id = serializers.PrimaryKeyRelatedField(queryset=Attribute.objects.all(), source="attribute", required=True)
values = serializers.ListField(
child=serializers.JSONField()
)
def validate(self, attrs):
attribute = attrs.get('id')
field = validators[attribute.attribute_type]
for v in attrs['values']:
field.run_validation(json.loads(v.replace("'", '"')))
return super().validate(attrs)
class BaseObjectApiInputSerializer(serializers.Serializer):
category_id = serializers.PrimaryKeyRelatedField(
queryset=Category.objects.all()
)
attributes = AttributesSerializer(many=True)
I'm building a GraphQL application in Python/Graphene using a MongoDB backend (through MongoEngine). Everything has been working well, but noticed that there's not a lot documentation for handling nested lists of embedded documents. I thought one power of GraphQL was the ability to project only the properties you want, but it doesn't appear to be the case fully.
Looking at this collection as an example:
[
{
"name": "John Doe",
"age": 37,
"preferences": [
{
"key": "colour",
"value": "Green"
},
{
"key": "smell",
"value": "onions cooking in butter"
},
...
]
},
...
]
If I want to find a particular object through GraphQL, I would look up through a query like
{
person(name: "John Doe"){edges{node{
name age preferences{edges{node{
key value
}}}
}}}
}
But this could bring back hundreds of nested documents. What I would like to do instead is to identify the requested nested documents as part of the projection request.
{
person(name: "John Doe"){edges{node{
name age preferences(key: "colour"){edges{node{
key value
}}}
}}}
}
My understanding reading the GraphQL spec is these sub-queries are not possible, but wanted to confirm with experts first. And if it is possible, how would I implement it to support these types of requests?
Update Maybe a schema example will provide some more insightful responses.
class PreferenceModel(mongoengine.EmbeddedDocument):
key = mongoengine.fields.StringField()
value = mongoengine.fields.StringField()
class Preference(graphene_mongo.MongoengineObjectType):
class Meta:
interfaces = (graphene.relay.Node, )
model = PreferenceModel
class PersonModel(mongoengine.Document):
meta = {'collection': 'persons'}
name = mongoengine.fields.StringField()
age = mongoengine.fields.IntField()
preferences = mongoengine.fields.EmbeddedDocumentListField(PreferenceModel)
class Person(graphene_mongo.MongoengineObjectType):
class Meta:
interfaces = (graphene.relay.Node, )
model = PersonModel
class Query(graphene.ObjectType):
person = graphene_mongo.MongoengineConnectionField(Person)
schema = graphene.Schema(query=Query, types=[Person])
app = starlette.graphql.GraphQLApp(schema=schema)
Using this above structure, what changes would be necessary to allow for queries/filters on nested objects?
I had a similiar issue but working graphene-django. I solved it using custom resolvers on the DjangoObjectType, like this:
import graphene
from graphene_django import DjangoObjectType
from .models import Question, Choice, SubChoice
class SubChoiceType(DjangoObjectType):
class Meta:
model = SubChoice
fields = "__all__"
class ChoiceType(DjangoObjectType):
sub_choices = graphene.List(SubChoiceType, search_sub_choices=graphene.String())
class Meta:
model = Choice
fields = ("id", "choice_text", "question")
def resolve_sub_choices(self, info, search_sub_choices=None):
if search_sub_choices:
return self.subchoice_set.filter(sub_choice_text__icontains=search_sub_choices)
return self.subchoice_set.all()
class QuestionType(DjangoObjectType):
choices = graphene.List(ChoiceType, search_choices=graphene.String())
class Meta:
model = Question
fields = ("id", "question_text")
def resolve_choices(self, info, search_choices=None):
if search_choices:
return self.choice_set.filter(choice_text__icontains=search_choices)
return self.choice_set.all()
class Query(graphene.ObjectType):
all_questions = graphene.List(QuestionType, search_text=graphene.String())
all_choices = graphene.List(ChoiceType, search_text=graphene.String())
all_sub_choices = graphene.List(SubChoiceType)
def resolve_all_questions(self, info, search_text=None):
qs = Question.objects.all()
if search_text:
qs = qs.filter(question_text__icontains=search_text)
return qs
def resolve_all_choices(self, info, search_text=None):
qs = Choice.objects.all()
if search_text:
qs = qs.filter(choice_text__icontains=search_text)
return qs
def resolve_all_sub_choices(self, info):
qs = SubChoice.objects.all()
return qs
schema = graphene.Schema(query=Query)
you can find the example here: https://github.com/allangz/graphene_subfilters/blob/main/mock_site/polls/schema.py
It may work for you
As per MongoEngine Documentation on Document Inheritence
I tried to create a base class as below
import datetime
from mongoengine import *
connect("testdb")
class Base(Document):
companyId = StringField(required=True)
creationDate = DateTimeField()
modifiedDate = DateTimeField()
meta = {'allow_inheritance': True}
def save(self, *args, **kwargs):
if not self.creationDate:
self.creationDate = datetime.datetime.now()
self.modifiedDate = datetime.datetime.now()
return super(Base, self).save(*args, **kwargs)
class Child1(Base):
# identifier = StringField(required=True, unique=True, primary_key=True)
createdBy = StringField(required=True)
class Child2(Base):
memberId = StringField(required=True)
Child1(companyId='ab', createdBy='123').save()
Child2(companyId='ab', memberId='123').save()
MY aim is to get two collections named Child1 and Child2 under "testdb", but instead only one collection getting created named 'base' with two documents in it.
{
"_id" : ObjectId("5656b66381f49543f27af85a"),
"_cls" : "Base.Child1",
"companyId" : "ab",
"creationDate" : ISODate("2015-11-26T13:06:01.689Z"),
"modifiedDate" : ISODate("2015-11-26T13:06:01.689Z"),
"createdBy" : "123"
}
/* 1 */
{
"_id" : ObjectId("5656b66381f49543f27af85b"),
"_cls" : "Base.Child2",
"companyId" : "ab",
"creationDate" : ISODate("2015-11-26T13:06:03.621Z"),
"modifiedDate" : ISODate("2015-11-26T13:06:03.621Z"),
"memberId" : "123"
}
How can I specify different collection name for my subclass?
versions
python 2.7.10
mongodb 3.0.3
mongoengine 0.8.7
pymongo 2.7.2
This is the expected behaviour according to the documentation you link.
To create a specialised type of a Document you have defined, you may subclass it and add any extra fields or methods you may need. As this is new class is not a direct subclass of Document, it will not be stored in its own collection; it will use the same collection as its superclass uses.
That being said to store in it own collection you need the base class must be an abstract class and you can optionally specify the child class collections' name using their meta attribute.
class Base(Document):
...
meta = {
'allow_inheritance': True,
'abstract': True
}
...
class Child1(Base):
...
meta = {'collection': 'child1'} # optional
I have a Django model that is like this:
class WindowsMacAddress(models.Model):
address = models.TextField(unique=True)
mapping = models.ForeignKey('imaging.WindowsMapping', related_name='macAddresses')
And two serializers, defined as:
class WindowsFlatMacAddressSerializer(serializers.Serializer):
address = serializers.Field()
class WindowsCompleteMappingSerializer(serializers.Serializer):
id = serializers.Field()
macAddresses = WindowsFlatMacAddressSerializer(many=True)
clientId = serializers.Field()
When accessing the serializer over a view, I get the following output:
[
{
"id": 1,
"macAddresses": [
{
"address": "aa:aa:aa:aa:aa:aa"
},
{
"address": "bb:bb:bb:bb:bb:bb"
}
],
"clientId": null
}
]
Almost good, except that I'd prefer to have:
[
{
"id": 1,
"macAddresses": [
"aa:aa:aa:aa:aa:aa",
"bb:bb:bb:bb:bb:bb"
],
"clientId": null
}
]
How can I achieve that ?
Create a custom serializer field and implement to_native so that it returns the list you want.
If you use the source="*" technique then something like this might work:
class CustomField(Field):
def to_native(self, obj):
return obj.macAddresses.all()
I hope that helps.
Update for djangorestframework>=3.9.1
According to documentation, now you need override either one or both of the to_representation() and to_internal_value() methods. Example
class CustomField(Field):
def to_representation(self, value)
return {'id': value.id, 'name': value.name}
Carlton's answer will work do the job just fine. There's also a couple of other approaches you could take.
You can also use SlugRelatedField, which represents the relationship, using a given field on the target.
So for example...
class WindowsCompleteMappingSerializer(serializers.Serializer):
id = serializers.Field()
macAddresses = serializers.SlugRelatedField(slug_field='address', many=True, read_only=True)
clientId = serializers.Field()
Alternatively, if the __str__ of the WindowsMacAddress simply displays the address, then you could simply use RelatedField, which is a basic read-only field that will give you a simple string representation of the relationship target.
# models.py
class WindowsMacAddress(models.Model):
address = models.TextField(unique=True)
mapping = models.ForeignKey('imaging.WindowsMapping', related_name='macAddresses')
def __str__(self):
return self.address
# serializers.py
class WindowsCompleteMappingSerializer(serializers.Serializer):
id = serializers.Field()
macAddresses = serializers.RelatedField(many=True)
clientId = serializers.Field()
Take a look through the documentation on serializer fields to get a better idea of the various ways you can represent relationships in your API.