graphene-django icon indicating copy to clipboard operation
graphene-django copied to clipboard

Cannot serialize a CharField with Choices and blank=True

Open cansin opened this issue 5 years ago • 5 comments
trafficstars

  • What is the current behavior?

Trying to return a value of a field such as models.CharField(choices=some_choices, blank=True, max_length=64) with a blank value from Graphene causes a graphql.error.base.GraphQLError: Expected a value of type "SomeChoice" but received:

Reproduction is at https://repl.it/repls/WaryRespectfulTrace#README.md

Given a model such as:

from django.db import models
from model_utils.models import UUIDModel
from django.utils.translation import gettext_lazy as _

class Person(UUIDModel, models.Model):
    class Gender(models.TextChoices):
        MALE = "male", _("Male")
        FEMALE = "female", _("Female")
        OTHER = "other", _("Other")

    gender = models.CharField(choices=Gender.choices, blank=True, max_length=64)

and a type such as:

from graphene_django import DjangoObjectType

class PersonType(DjangoObjectType):
    class Meta:
        model = Person
        use_connection = True

and a query such as:

import graphene

class Query(graphene.ObjectType):
    person = graphene.Field(PersonType, id=graphene.UUID())

    @classmethod
    def resolve_person(cls, root, info, id):
        return Person.objects.get(id=id)

Running a GraphQL such as:

query {
    person(id: "some-uuid") {
        id
        gender
    }
}

results in a graphql.error.base.GraphQLError: Expected a value of type "PersonGender" but received: iff the person's gender value is blank.

The stacktrace is:

Traceback (most recent call last):
  File "/Users/user/.virtualenvs/code/lib/python3.7/site-packages/promise/promise.py", line 87, in try_catch
    return (handler(*args, **kwargs), None)
  File "/Users/user/.virtualenvs/code/lib/python3.7/site-packages/graphql/execution/executor.py", line 532, in <lambda>
    exe_context, return_type, field_asts, info, path, resolved
  File "/Users/user/.virtualenvs/code/lib/python3.7/site-packages/graphql/execution/executor.py", line 563, in complete_value
    return complete_leaf_value(return_type, path, result)
  File "/Users/user/.virtualenvs/code/lib/python3.7/site-packages/graphql/execution/executor.py", line 633, in complete_leaf_value
    path=path,
graphql.error.base.GraphQLError: Expected a value of type "PersonGender" but received: 
  • What is the expected behavior?

The empty value should be successfully returned.

  • What is the motivation / use case for changing the behavior?

Left unfixed, the clients are receiving an erroneous error (pun intended 😛). This is especially problematic when we are trying to distinguish between an error state vs a valid state.

  • Please tell us about your environment:

    • Version:
Django==3.0.7
django-cors-headers==3.2.1
django-environ==0.4.5
django-extensions==2.2.9
django-filter==2.2.0
django-graphql-jwt==0.3.1
django-heroku==0.3.1
django-model-utils==4.0.0
django-polymorphic==2.1.2
django-redis-cache==2.1.0
django-rq==2.3.0
django-sendgrid-v5==0.8.1
graphene==2.1.8
graphene-django==2.10.1
graphene-stubs==0.15
graphql-core==2.3.2
graphql-relay==2.0.1
  • Platform:
Python 3.7.3
  • Other information (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. stackoverflow)

I realize a similar issue was previously reported at https://github.com/graphql-python/graphene-django/issues/503 and https://github.com/graphql-python/graphene-django/pull/714 and fixed at https://github.com/graphql-python/graphene-django/pull/714 .

cansin avatar Jul 14 '20 22:07 cansin

Setting the field type explicitly such as:

import graphene
from graphene_django import DjangoObjectType

GenderType = graphene.Enum.from_enum(Person.Gender)

class PersonType(DjangoObjectType):
    class Meta:
        model = Person
        use_connection = True

    gender = GenderType()

also does not fix the issue.

cansin avatar Jul 14 '20 22:07 cansin

I have realized, introducing null=True, default=None to the CharField fixes the issue. But that is against the best practices described by Django's documentation per https://docs.djangoproject.com/en/3.0/ref/models/fields/#null . But at least we are able to get around the problem with that approach.

cansin avatar Jul 15 '20 00:07 cansin

@cansin thanks for the detailed error report. This does look like a bug and I'll look into it as soon as possible. As a workaround you can update your PersonType DjangoObjectType with a custom resolver for the gender field:

class PersonType(DjangoObjectType):
    class Meta:
        model = Person
        use_connection = True

    def resolve_gender(person, info):
      if person.gender:
        return person.gender
      return None

jkimbo avatar Jul 15 '20 15:07 jkimbo

I can confirm the bug. I am also experiencing it in the same way when using enum based choices on a CharField with blank=True.

Thanks @jkimbo for the custom renderer workaround. ;)

Eraldo avatar Jul 22 '20 02:07 Eraldo

I've run into this as well. It seems that another workaround is to add an "empty" choice to the set of choices:

field_choices = (
    ('', '--------'),
    ('foo', 'Foo'),
    ('bar', 'Bar'),
)

However, this is redundant to setting blank=True on the field and IMO should be avoided. Would love to see the field convertor pick up blank=True and adjust accordingly on its own.

jeremystretch avatar Jun 29 '21 16:06 jeremystretch