Search code examples
djangoasynchronousdjango-rest-frameworkdjango-channels

Django Rest Framework and Channels, You cannot call this from an async context


What i am trying to do is to nest a DRF model serializer into another model serialiser's field like so

class username_serial(ModelSerializer):
    class Meta:
        model = User
        fields = ['username','email']


class game_serial(ModelSerializer):

    user_01 = username_serial()

    class Meta:
        model = game
        fields = ['id','user_01','user_02','is_private','is_accepted']

Error :

Exception inside application: You cannot call this from an async context - use a thread or sync_to_async. Traceback (most recent call last): File "C:\Users\baza\Desktop\production\venv\lib\site-packages\django\db\models\fields\related_descriptors.py", line 173, in get rel_obj = self.field.get_cached_value(instance) File "C:\Users\baza\Desktop\production\venv\lib\site-packages\django\db\models\fields\mixins.py", line 15, in get_cached_value return instance._state.fields_cache[cache_name] KeyError: 'user_01'

This works normally without Django Chennels because channels is async and i can't use sync code with, works fine by using:

os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"

In the settings file but it's not a safe approach when it comes to production. i tried using channel's database_sync_to_async as a decorator and as well as a function with a SerializerMethodField like so:

class game_serial(ModelSerializer):
    user_01 = SerializerMethodField(read_only=True)

    @database_sync_to_async
    def get_user_01(self, obj):
        username = obj.user_01.username

        return str(username)
    class Meta:
        model = game
        fields = ['id','user_01','user_02','is_private','is_accepted']

but i get back:

[OrderedDict([('id', 23), ('user_01', <coroutine object SyncToAsync.__call__ at 0x0000000005DF6678>), ('user_02', None), ('is_private', False), ('is_accepted', False)]), OrderedDict([('id', 24), ('user_01', <coroutine object SyncToAsync.__call__ at 0
x0000000005DF6D58>), ('user_02', None), ('is_private', False), ('is_accepted', False)])]

with an

Exception inside application: Object of type 'coroutine' is not JSON serializable

so i guess that JSON couldn't serialize those "coroutine" objects and normally i should or "want" to get the actual values of them instead.

How can i do the task? any workaround or other methods are welcomed, thank you in advance.

in consumers.py ..

games = await database_sync_to_async(self.get_games)()
        serialized_games = game_serial(games, many=True)
        await self.send({
            "type": "websocket.send",
            'text': json.dumps(serialized_games.data)
        })
def get_games(self):
        return list(game.objects.all())

Solution

  • I never used Django Channels but I know Django and async. I'll be honest, I don't like these hacky decorators. And I don't think running such a simple task in a thread is a good idea.

    You have an obj, so you were able to query the DB earlier. If so, there's a place without async context or async context where accessing the DB works. In the error message user_01 is not found in the "cached" object from the DB. So just prefetch what you need before.

    def get_queryset(self):
        return game_serial.objects.select_related('user_01')
    
    class game_serial(ModelSerializer):
        user_01 = serializers.CharField(source='user_01.username')
    

    This way you don't have problems with this sync-to-async magic, it's more efficient and easier to reason about.


    EDIT:

    I repeat, you should select related where you fetch the data. After you added another example, I can suggest something like that

    def get_games(self):
        return list(game.objects.select_related('user_01').all())
    

    and it will work just fine.

    You can also try

    @database_sync_to_async
    def get_games(self):
        return list(game.objects.select_related('user_01').all())
    

    and

    serialized_games = await game_serial(games, many=True)
    

    In both cases this serializer will work just fine.

    class game_serial(ModelSerializer):
        user_01 = serializers.CharField(source='user_01.username')
    
        class Meta:
            model = game
            fields = ['id','user_01','user_02','is_private','is_accepted']